悲观锁和乐观锁
目录
一 概念
无论是悲观锁还是乐观锁,都是人们定义出来的概念,仅仅是一种思想,与语言无关
1.1 什么是并发控制
1 并发控制:当程序中出现并发问题时,就需要保证在并发情况下数据的准确性,以此确保当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的,这种手段就叫做并发控制
2 没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题
1.2 悲观锁
悲观锁:当要对一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发,让并行变成串行
-这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写PCC,又名悲观锁】
- 之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。
-悲观锁实现方式:
1 传统的关系型数据库(如mysql)使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
2 编程语言中的线程锁,比如python的互斥锁
3 分布式锁:redis实现的分布式锁等
1.3 乐观锁
通过程序实现(没有真正的一把锁),不会产生死锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果更新了,我们就不做修改
-乐观锁实现方案:
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.4 悲观锁乐观锁使用场景
并发量:如果并发量不大,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,建议乐观锁
响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高
读多写少: 乐观锁适用于读多写少的应用场景,这样可以提高并发粒度
冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大
重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低
二 MySQL实现悲观锁
start transaction; # 开启事务
# begin; # 开启事务,简写
select * from goods where id = 1 for update; # 行锁,对于查询出的一条或者多条结果加行锁(for update是表示加锁)
# order表中加数据
update goods set stock = stock - 1 where id = 1; # 更新
commit; #提交事务
三 django实现悲观锁乐观锁案例
# 线上卖图书
-图书表 图书名字,图书价格,库存字段
-订单表: 订单id,订单名字
# 表准备
class Book(models.Model): # 书籍表
name = models.CharField(max_length=32)
price = models.IntegerField()
count = models.SmallIntegerField(verbose_name='库存')
class Order(models.Model): # 订单表
order_id = models.CharField(max_length=64)
order_name = models.CharField(max_length=32)
# 使用mysql
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'lqz',
'HOST': '127.0.0.1',
'PORT': '3306',
'USER': 'lqz',
'PASSWORD': '123',
}
}
# 创建lqz数据库,执行数据迁移命令
python3.9 manage.py makemigrations
python3.9 manage.py migrate
3.1 django实现悲观锁
#1 使用悲观锁实现下单
@transaction.atomic # 整个过程在一个事物中,所以在函数上方加事务装饰器--->改两个表:book表减库存,订单表生成记录
def seckill(request):
# 锁住查询到的book对象,直到事务结束
sid = transaction.savepoint() # 保存点
# 悲观锁: select_for_update()
# 加锁了-->行锁还是表锁? 分情况,都有可能
# 先查询出修改的对象
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('库存不足,秒杀失败')
3.2 乐观锁普通版,秒杀 --> 库存还有,有的人就没成功
# 2 乐观锁秒杀--普通版
@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: # 影响的行数是1,表示修改成功
transaction.savepoint_commit(sid) # 提交保存点
return HttpResponse('秒杀成功')
else: # 修改不成功,回滚
transaction.savepoint_rollback(sid)
return HttpResponse('被别人改了,回滚,秒杀失败')
else:
transaction.savepoint_rollback(sid)
return HttpResponse('库存不足,秒杀失败')
# 但是会产生一个情况:库存还有,有的人就没成功
3.3 升级版
@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('库存不足,秒杀失败')