鎖的應用+redis分散式鎖
阿新 • • 發佈:2020-12-29
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秒,不同鎖有不同的應用場景,這才是重中之中