一. 乐观锁和悲观锁概念
1.1 悲观锁
- 总是假设最坏的情况,每次读取数据的时候总是认为其他线程会修改,所以都会加锁(读锁,写锁,行锁),当其他线程想要操作数据时,都需要阻塞挂起,等这个线程释放锁之后,其他线程才可以处理。悲观锁可以依靠数据库来实现。
- 悲观锁的例子:InnoDB存储引擎的MySQL数据库事务的隔离等级(MySQL默认使用可重复读(Repeatable read), 两个用户同时操作表中的同一行数据的时候,使用事务处理的时候,会对mysql进行加锁和释放锁,只有一个用户操作完成后,另外的用户才可以操作这行数据。这种方式是悲观锁的方式。
1.2 乐观锁
-
总是认为不会产生并发问题,每次去读取的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新或者修改这条数据的时候,会判断在这之前有没有其他线程对数据进行修改,一般会使用版本号机制或者CAS操作实现。
-
版本号的方式:一般在数据表中加一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据时,在读取数据的同时,读取version值记录下来,在提交更新的时候,读取到当前数据库中的version值和之前读取的值相同的时候才会更新,否则重试更新操作,直到更新成功。
update table set name='xiaoming', version=version+1 where id=1 and version = old_version;
-
CAS 算法:即compare and swap和 compare and set,(比较再交换)
- CAS有3个操作数:内存值V(备份的旧数据),旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,讲内存值修改为B,否则什么都不做。
- CAS的底层原理:
- 保证原子性的两种方式:通过总线锁来保证原子性(总线锁就是使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将会被阻塞住。);通过缓存锁来保证原子性,(如果某个内存区域数据,已经同时被两个或以上处理器核缓存,缓存锁就会通过缓存一致性机制阻止对其修改,以此来保证操作的原子性,当其他处理器核回写已经被锁定的缓存行的数据时会导致该缓存行无效。就是说当某块CPU核对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取。)如果想深入的理解,请搜索缓存一致性的问题。
- ABA问题:
- 变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
- 循环的时间太长, 如果不成功就一直循环执行到成功,如果长时间不成功,会给CPU带来非常大的执行开销。
- 只能保证一个共享变量原子操作
-
二. 乐观锁和悲观锁的使用场景
- 乐观锁适用于读比较多,写比较少的情况。锁冲突的情况很少,提高了系统读的并发量。
- 写比较多的场景用悲观锁比较合适。
- 在高并发的情况下,乐观锁的性能要高于悲观锁
三. 乐观锁和悲观锁的实现例子
-
乐观锁的实现
-
下单的例子(MySQL数据库)
-- 查询不商品信息 select (status, status, version) from t_goods where id=#{id} -- 根据商品信息生成订单 -- 修改商品的status update t_goods set status=2, version=version+1 where id=#{id} and version=#{version}
-
Django乐观锁实现高并发下单操作,使用乐观锁要修改MySQL的默认的事务等级
-
from django.db import transaction
# 部分代码使用事物
with transaction.atomic():
# 创建保存点
save_id = transaction.savepoint()
try:
# 生成订单信息
order = OrderInfo.objects.create(
...
)
# 从redis取购物车要结算的商品数据(省略写法)
...
cart = {shu.id: sku_count, ...}
while True: # 直到没有库存为止
sku = SKU.objects.get(pk=sku_id) # 不加锁查询
# 购物车商品数量
count = cart[shu.id]
# 获取原始库存
origin_stock = sku.stock
# 判断商品库存是否充足
if origin_stock < count:
# 库存不足
raise serializers.ValidationError('库存不足')
# 演示并发请求
import time
time.sleep(5)
# 记录原来的值
origin_sales = sku.sales
# 计算要更新的值
new_stock = origin_stock - count
new_sales = origin_sales + count
# 返回受影响的行数
ret = SKU.objects.filter(id=sku.id,stock=origin_stock).update(stock=new_stock,sales=new_sales)
if ret == 0:
# 更新成功,进入下次循环重新判断
continue
else:
# 保存订单商品数据
...
# 更新成功,退出while循环
break
...
except Exception as e:
# 提交保存点
transaction.savepoint_rollback(save_id)
raise serializers.ValidationError('下单失败')
# 提交事务
transaction.savepoint_commit(save_id)
# 清除购物车中已经结算的商品
...
return order
- 悲观锁的实现
- 悲观锁的实现,有:共享锁, 排它锁
- 悲观锁在MySQL数据库本身实现。
- 共享锁:对数据库中的资源,自身可以读取该资源,其他人也可以读取该资源,但无法修改,要想修改,必须等所有共享锁都释放完之后。
- 排它锁:对某一资源,自己拿到锁后,自身可以进行增删改查,其他人无法进行修改。
-- 0.开始事务 begin;/begin work;/start transaction; -- (三者选一就可以) -- 1.查询出商品信息 select status from table where id=1 for update; -- 2.根据商品信息生成订单 insert into table111 (id,goods_id) values (null,1); -- 3.修改商品status为2 update table set status=2 where id=1; -- 4.提交事务 -- 在该连接的事务提交之前,其他人无法修改d=1这行的数据,直到这个事务提交之后,锁释放之后,其他人才可以操作这行数据,innoDB的MySQL的默认行锁机制。 commit;/commit work; -- (任选一种释放锁)