1. 程式人生 > 實用技巧 >鎖的應用+redis分散式鎖

鎖的應用+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秒,不同鎖有不同的應用場景,這才是重中之中