锁的应用+redis分布式锁

1 引言

  • 思考:高并发情况,会不会出现问题?
from django.db import connection

# 锁的使用
def testlock(request):
    res = User.objects.get(pk=1)
    # 查不到会报错
    if res.num > 0:
        time.sleep(5)
        # 人为延缓流程
        with connection.cursor() as c:
            c.execute('update user set num = num - 1 where id = 1')
        return HttpResponse('ok')
    else:
        return HttpResponse('钱包为空')
  • 什么是qbs

引入xampp(Apache+MySQL+PHP+PERL),是一种很好的压测工具,不用认为压测,其中有一个指令对qbs的解释很好

ab -c100 -n500
# -c:并发   -n:500人
# 500/100 5s完成请求(通常是1:4 1:5)

这就说明了,高并发情况下,逻辑是对的,但是代码会出现问题

2 锁的概念

2.1 本地锁

  • 线程是不需要加锁的,因为在Cpython中存在GIL全局解释器锁
# 本地锁
def change_it(n):
    global num
    
    if lock.acquire():    
        try:
            for i in range(1000000):
                num = num + n
                num = num - n
        finally:
            lock.release()
    print(num)
threads - [
    threading.Thread(target=change_it, args=(8,)),
    threading.Thread(target=change_it, args=(10,))
]
lock = threading.Lock()
[t.start() for t in threads]
[t.join() for t in threads]

2.2 分布式锁

  • 共用一把锁(全局锁,基于redis)
setnx locknx test
# key locknx value test
setnx locknx task
# 发现无法更改
0
# 证明:获取
get locknx
'test'
# 释放锁
del locknx
1

# 56行bind 127.0.0.1 注释掉,允许别人访问自己的redis
# 不会引发资源冲突
# 隐患:如果正在访问的时候宕机,永久锁入
# 加一个延时锁
expire locknx 10

3 mysql + django 实现锁机制

3.1 mysql中常见的锁

3.1.1 乐观锁

不操作不加锁,读取不加

3.1.2 悲观锁

读取就加锁,持悲观态度

3.2 启发文件

@contextmanager
def add_lock(lock_id, expire_second, **kwargs):

    '''
    :param lock_id: 锁id
    :param expire_second: 过期时间
    :param kwargs: 用于以后自定义传入参数
    :return:
    '''

    dead_lock = False

    # 加锁
    while True:
        try:
            MyLock.objects.create(id=lock_id)
            break
        except django.db.utils.IntegrityError:
            # 出错看是在运行中还是死锁
            ctime = datetime.datetime.now() - datetime.timedelta(seconds=expire_second)
            if MyLock.objects.filter(Q(id=lock_id) & Q(create_time__gt=ctime)).exists():
                print('当前任务执行中')
                # time.sleep(5)
                continue
            else:
                # 排除刚好任务执行完的那一刻,还未释放锁就加锁的情况
                if not dead_lock:
                    dead_lock = True
                    time.sleep(5)
                # 删除死锁
                MyLock.objects.filter(id=lock_id).delete()
                print('死锁')
                continue
    # 执行任务
    yield kwargs
    # 去锁
    MyLock.objects.filter(id=lock_id).delete()

3.3 具体代码

3.3.1 models.py
class MyLock(models.Model):
    id = models.AutoField(auto_created=True, primary_key=True, serialize=False,verbose_name='ID')
    create_time = models.DateTimeField(auto_now=True, verbose_name='创建锁时间')

    class Meta:
        db_table = 'my_lock_model'
3.3.2 views.py
class WalletView(APIView):
    def get(self, request):
            dead_lock = False
            # 加锁
            while True:
                try:
                    lock = MyLock.objects.filter(id=1)
                    if lock:
                        print('程序运行中,请等待')
                        time.sleep(5)
                        continue
                    else:
                        MyLock.objects.create(id=1)
                        break
                except django.db.utils.IntegrityError:
                    # 出错看是在运行中还是死锁
                    ctime = datetime.datetime.now() - datetime.timedelta(seconds=60)
                    if MyLock.objects.filter(Q(id=1) & Q(create_time__gt=ctime)).exists():
                        print('当前任务执行中')
                        continue
                    else:
                        # 排除刚好任务执行完的那一刻,还未释放锁就加锁的情况
                        if not dead_lock:
                            dead_lock = True
                            time.sleep(5)
                        # 删除死锁
                        MyLock.objects.filter(id=1).delete()
                        print('死锁')
                        continue
            user_id = 3
            give_money = 1
            wallet = WalletModel.objects.get(user_id=user_id)
            if give_money <= wallet.money:
                WalletModel.objects.filter(user_id=user_id).update(money=F('money') - give_money)
            print('释放锁')
            MyLock.objects.filter(id=1).delete()

            return Response({'msg': '提现成功', 'code': 200})

3.4 xampp测试结果

3.4.1 测试

3.4.2 结果

4 redis + django 实现分布式锁

4.1 分布式锁

分布式锁的本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试。占坑一般使用setnx指令,只允许一个客户端占坑。先来先占,用完了再调用del指令释放坑。先来先占,用完了再调用del指令释放坑。为了解决死锁,引入expire过期时间。(这种竞争解决方案还可以考虑消息队列)

4.2 具体代码

4.2.1 views.py
class WalletView(APIView):
    def post(self, request):
        user_info = decodeToken(request)
        user_id = user_info.get('user_id')
        user = WalletModel.objects.filter(user_id=user_id).first()
        try:
            money = float(request.data.get('money'))
        except Exception as e:
            return Response({'msg': '充值错误', 'code': 400, 'error': e})
        if user:
               from decimal import Decimal
               # decimal 和 float 不能叠加,要把 float 转换
               user.money += Decimal(money)
               user.save()
        else:
            WalletModel.objects.create(money=money, user_id=user_id)

        if money < 100:
            return Response({'msg': '充值成功', 'code': 200})
        else:
            coupon_code = create_code(4)
            if 100 <= money < 300:
                CouponModel.objects.create(code=coupon_code, coupon='3', user_id=user_id)
                return Response({'msg': '充值成功,同时您获取了10元无门槛红包哦', 'code': 200})
            elif 300 <= money < 500:
                CouponModel.objects.create(code=coupon_code, coupon='1', user_id=user_id)
                return Response({'msg': '充值成功,同时您获取了一张满300减50的优惠券', 'code': 200})
            elif 500 <= money < 1000:
                CouponModel.objects.create(code=coupon_code, coupon='2', user_id=user_id)
                return Response({'msg': '充值成功,同时您获取了终生八折优惠', 'code': 200})


    def put(self, request):
        import redis
        r = redis.Redis(db=15, decode_responses=True)
        dead_lock = True
        while True:
            try:
                r.setnx('locknx', 'lock')
                r.expire('locknx', 20)
                break
            except django.db.utils.IntegrityError:
                # 出错看是在运行中还是死锁
                if r.get('locknx'):
                    print('当前任务执行中')
                    time.sleep(5)
                    continue
                else:
                    # 排除刚好任务执行完的那一刻,还未释放锁就加锁的情况
                    if not dead_lock:
                        dead_lock = True
                        time.sleep(5)
                    # 删除死锁
                    r.delete('locknx')
                    print('死锁')
                    continue
        user_info = decodeToken(request)
        user_id = user_info.get('user_id')
        give_money = float(request.data.get('money'))
        wallet = WalletModel.objects.get(user_id=user_id)
        if give_money <= wallet.money:
            WalletModel.objects.filter(user_id=user_id).update(money=F('money')-give_money)
        r.delete('locknx')
        return Response({'msg': '提现成功', 'code': 200})

4.3 xampp测试结果

4.3.1 测试

4.3.2 结果

5 小结

  • 相比之下

redis性能好,测试mysql有的时候会失败,而且跑完100并发需要86秒,但是redis只需要9秒,不同锁有不同的应用场景,这才是重中之中

原文地址:https://www.cnblogs.com/mapel1594184/p/14208449.html