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秒,不同锁有不同的应用场景,这才是重中之中