[1 悲观锁乐观锁](#1 悲观锁乐观锁)
[1.1 并发控制](#1.1 并发控制)
[1.1 悲观锁](#1.1 悲观锁)
[1.2 乐观锁](#1.2 乐观锁)
[1.3 悲观锁乐观锁使用场景](#1.3 悲观锁乐观锁使用场景)
[2 django中开启事务](#2 django中开启事务)
[2.1 全局开启事务](#2.1 全局开启事务)
[2.2 视图开启事务](#2.2 视图开启事务)
[2.3 局部使用事务](#2.3 局部使用事务)
[2.4 savepoint回滚](#2.4 savepoint回滚)
[2.5 事务提交后回调函数](#2.5 事务提交后回调函数)
[3 django中使用悲观锁](#3 django中使用悲观锁)
[3.1 前置条件,表模型](#3.1 前置条件,表模型)
[3.1 模拟秒杀生成订单场景](#3.1 模拟秒杀生成订单场景)
[4 django使用乐观锁](#4 django使用乐观锁)
[4.1 普通版](#4.1 普通版)
[4.2 升级版](#4.2 升级版)
1 悲观锁乐观锁
无论是悲观锁还是乐观锁,都是人们定义出来的概念,仅仅是一种思想,与语言无关
1.1 并发控制
sql
1 并发控制:当程序中出现并发问题时,就需要保证在并发情况下数据的准确性,以此确保当前用户
和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的,这种手段就叫做并发控制
2 没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题
1.1 悲观锁
sql
# 悲观锁是什么?
1 悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,
最好的办法就是直接对该数据进行加锁以防止并发。
2 这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制
【Pessimistic Concurrency Control,缩写"PCC",又名"悲观锁"】
3 之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。
总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,
因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。
# 悲观锁的实现方式:
1 传统的关系型数据库(如mysql)使用这种锁机制,
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
2 编程语言中的线程锁,比如python的互斥锁
3 分布式锁:redis实现的分布式锁等
1.2 乐观锁
sql
# 乐观锁是什么?
1 通过程序实现,不会产生死锁
2 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,
只在更新的时候会判断一下在此期间别人有没有去更新这个数据
# 乐观锁实现方式
1 CAS(Compare And Swap) 即比较并交换。
是解决多线程并行情况下使用锁造成性能损耗的一种机制,
CAS 操作包含三个操作数------内存位置(V)、预期原值(A)和新值(B),执行CAS操作的时候,
将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,
否则,处理器不做任何操作
CAS会出现ABA问题,比如,你看到桌子上有100块钱,然后你去干其他事了,回来之后看到桌子上
依然是100块钱,你就认为这100块没人动过,其实在你走的那段时间,别人已经拿走了100块,
后来又还回来了。这就是ABA问题
解决ABA问题:
既然有人动了,那我们对数据加一个版本控制字段,只要有人动过这个数据,就把版本进行增加,
我们看到桌子上有100块钱版本是1,回来后发现桌子上100没变,但是版本却是2,
就立马明白100块有人动过
2 版本号控制:
在数据表中增加一个版本号字段,每次更新数据时将版本号加1,同时将当前版本号作为更新条件,
如果当前版本号与更新时的版本号一致,则更新成功,否则更新失败
3 时间戳方式:
在数据表中增加一个时间戳字段,每次更新数据时将时间戳更新为当前时间戳,
同时将当前时间戳作为更新条件,如果当前时间戳与更新时的时间戳一致,则更新成功,否则更新失败
1.3 悲观锁乐观锁使用场景
并发量:如果并发量不大,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,
悲观锁定会带来非常大的性能问题, 建议乐观锁
响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,
不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高
冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。
冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大
重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低
读多写少: 乐观锁适用于读多写少的应用场景,这样可以提高并发粒度
2 django中开启事务
sql
Django是支持事务操作的,它的默认事务行为是自动提交,
具体表现形式为:每次数据库操作(比如调用save()方法)会立即被提交到数据库中。
但是如果你希望把连续的SQL操作包裹在一个事务里,就需要手动开启事务。
2.1 全局开启事务
sql
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'lqz',
'HOST': '127.0.0.1',
'PORT': '3306',
'USER': 'lqz',
'PASSWORD': 'lqz123',
#全局开启事务,绑定的是http请求响应整个过程
'ATOMIC_REQUESTS': True,
}
}
1 每当有请求过来时,Django会在调用视图方法前开启一个事务。
如果完成了请求处理并正确返回了结果,Django就会提交该事务。否则,Django会回滚该事务。
2 虽然全局开启事务很简单,但Django并不推荐开启全局事务。因为一旦将事务跟 HTTP 请求绑定到
一起时,每一个请求都会开启事务,当访问量增长到一定的时候会造成很大的性能损耗。
在实际开发过程中,很多GET请求根本不涉及到事务操作,一个更好的方式是局部开启事务按需使用
局部禁用全局事务
sql
from django.db import transaction
# 局部禁用事务
@transaction.non_atomic_requests
def seckill(request):
return HttpResponse('秒杀成功')
# 多数据库,用default的视图不受事务控制
@transaction.non_atomic_requests(using='default')
def seckill(request):
return HttpResponse('秒杀成功')
2.2 视图开启事务
sql
Django局部开启事务,借助于transaction.atomic
使用它我们就可以创建一个具备原子性的代码块,一旦代码块正常运行完毕,
所有的修改会被提交到数据库。反之,如果有异常,更改会被回滚
sql
# fbv开启
from django.db import transaction
@transaction.atomic
def seckill(request):
return HttpResponse('秒杀成功')
# cbv开启
from django.db import transaction
from rest_framework.views import APIView
class SeckillAPIView(APIView):
@transaction.atomic
def post(self, request):
pass
2.3 局部使用事务
sql
from django.db import transaction
def seckill(request):
with transaction.atomic():
pass
return HttpResponse('秒杀成功')
2.4 savepoint回滚
sql
'''
在事务操作中,我们还会经常显式地设置保存点(savepoint)
一旦发生异常或错误,我们使用savepoint_rollback方法让程序回滚到指定的保存点
如果没有问题,就使用savepoint_commit方法提交事务
'''
from .models import Book
from django.db import transaction
def seckill(request):
with transaction.atomic():
# 设置回滚点,一定要开启事务
sid = transaction.savepoint()
print(sid)
try:
book = Book.objects.get(pk=1)
book.name = '红楼梦'
book.save()
except Exception as e:
# 如发生异常,回滚到指定地方
transaction.savepoint_rollback(sid)
print('出异常了,回滚')
# 如果没有异常,显式地提交一次事务
transaction.savepoint_commit(sid)
return HttpResponse('秒杀成功')
2.5 事务提交后回调函数
sql
# 有的时候我们希望当前事务提交后立即执行额外的任务,比如客户下订单后立即邮件通知卖家
###### 案例一##################
def send_email():
print('发送邮件给卖家了')
def seckill(request):
with transaction.atomic():
# 设置回滚点,一定要开启事务
sid = transaction.savepoint()
print(sid)
try:
book = Book.objects.get(pk=1)
book.count = book.count-1
book.save()
except Exception as e:
# 如发生异常,回滚到指定地方
transaction.savepoint_rollback(sid)
else:
transaction.savepoint_commit(sid)
transaction.on_commit(send_email)
return HttpResponse('秒杀成功')
##### 案例二:celery中使用###
transaction.on_commit(lambda: send_sms.delay('电话号码'))
3 django中使用悲观锁
3.1 前置条件,表模型
sql
from django.db import models
class Book(models.Model):
name = models.CharField(max_length=32)
count = models.IntegerField()
class Order(models.Model):
order_id=models.CharField(max_length=64)
order_name=models.CharField(max_length=32)
sql
# Django中使用悲观锁锁定一个对象,需要使用select_for_update()方法。它本质是一个行级锁,能锁定所有匹配的行,如果查询所有,可以锁住整张表,直到事务结束
@transaction.atomic
def seckill(request):
# 锁住查询到的book对象,直到事务结束
book = Book.objects.select_for_update().filter(pk=1).first()
book.count= book.count-1
book.save()
time.sleep(4)
return HttpResponse('秒杀成功')
3.1 模拟秒杀生成订单场景
sql
@transaction.atomic
def seckill(request):
# 锁住查询到的book对象,直到事务结束
sid = transaction.savepoint()
book = Book.objects.select_for_update().filter(pk=1).first() # 加悲观锁
if book.count > 0:
print('库存可以,下单')
Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
# 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
time.sleep(random.randint(1, 4)) # 模拟延迟
book.count=book.count-1
book.save()
transaction.savepoint_commit(sid)
return HttpResponse('秒杀成功')
else:
transaction.savepoint_rollback(sid)
return HttpResponse('库存不足,秒杀失败')
客户端测试
sql
import requests
from threading import Thread
def task():
res = requests.get('http://127.0.0.1:8000/sckill/')
print(res.text)
if __name__ == '__main__':
for i in range(10):
t=Thread(target=task)
t.start()
4 django使用乐观锁
4.1 普通版
sql
@transaction.atomic
def seckill(request):
# 锁住查询到的book对象,直到事务结束
sid = transaction.savepoint()
book = Book.objects.filter(pk=1).first() # 没加锁
count = book.count
print('现在的库存为:%s' % count)
if book.count > 0:
print('库存可以,下单')
Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
# 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
time.sleep(random.randint(1, 4)) # 模拟延迟
res = Book.objects.filter(pk=1, count=count).update(count=count - 1)
if res >= 1: # 表示修改成功
transaction.savepoint_commit(sid)
return HttpResponse('秒杀成功')
else: # 修改不成功,回滚
transaction.savepoint_rollback(sid)
return HttpResponse('被别人改了,回滚,秒杀失败')
else:
transaction.savepoint_rollback(sid)
return HttpResponse('库存不足,秒杀失败')
客户端测试
sql
import requests
from threading import Thread
def task():
res = requests.get('http://127.0.0.1:8000/sckill/')
print(res.text)
if __name__ == '__main__':
for i in range(10):
t=Thread(target=task)
t.start()
4.2 升级版
sql
@transaction.atomic
def seckill(request):
# 锁住查询到的book对象,直到事务结束
### 乐观锁可能会失败,我们一直尝试秒杀,直到秒成功或库存不足
while True:
sid = transaction.savepoint()
book = Book.objects.filter(pk=1).first() # 没加锁
count = book.count
print('现在的库存为:%s' % count)
if book.count > 0:
print('库存可以,下单')
Order.objects.create(order_id=str(datetime.datetime.now()), order_name='测试订单')
# 库存-1,扣减的时候,判断库存是不是上面查出来的库存,如果不是,就回滚
# time.sleep(random.randint(1, 4)) # 模拟延迟
res = Book.objects.filter(pk=1, count=count).update(count=count - 1)
if res >= 1: # 表示修改成功
transaction.savepoint_commit(sid)
return HttpResponse('秒杀成功')
else: # 修改不成功,回滚
transaction.savepoint_rollback(sid)
print('被别人扣减了,继续秒杀')
continue
else:
transaction.savepoint_rollback(sid)
return HttpResponse('库存不足,秒杀失败')