目录
随着信息化技术的发展,近年来越来越多的电商平台呈现在人们的视线中,一个好的电商平台自然能够对外稳定持续的运行,为大众提供便捷的足不出户线上购买商品服务,但是当用户量达到一定的数量之后,经典的库存超卖问题是必须要考虑的。
基于以上的一个场景,我们将展开一系列的分布式锁的了解,学习,深入实战过程。

环境要求:jdk1.8、maven3.x
技术要求:springboot2.x、mybatis、spring-data-redis、mysql、redis
其他:zookeeper、lua脚本、juc等

如果所示当存在多个线程并发访问同一个数据库资源时,可能会产生并发问题,例如超卖现象,在,在同一个服务中,我们可以采用jvm本地锁(synchronized锁或者ReentrantLock)来解决。但是jvm本地锁在三种情况下会导致失效。
jmter并发测试统一采用如下模拟参数:
100 用户量,每个用户量发送50次请求。
springboot中默认为单例模式,我们可以在在service类上增加@Scope(value=“prototype”,proxyMode=ScopeProxyMode.TARGET_CLASS)注解 , 使该类成为cglib代理的多例模式。

在多例模式下我们执行测试用例,发现产生了多卖现象,是因为获取的锁不是同一个对象锁导致。
在方法上增加@Transactional注解

由于增加了事务注解,代码处理流程变更为开启事务,获取锁,查询库存,扣减库存,释放锁,提交事务。
| 线程1 | 线程2 |
| begin开启事务 | begin开启事务 |
| 获取锁 | |
| 查询库存,库存数量为21 | |
| 扣减库存,库存数量为20 | |
| 释放锁 | |
| (此时事务还未提交) | 获取锁 |
| 查询库存,由于线程1事务还没提交, 查询到的库存数量还是21 | |
| 扣减库存,这个时候线程1 和线程2查询到的库存都是21, 就会产生并发问题了。 | |
| 释放锁 | |
| 提交事务 | 提交事务 |
默认情况下,mysql的事务隔离级别是可重复读,REPEATABLE_READ, 所以线程2读取不到未提交事务的数据,也就是读取的还是线程1扣减库存之前的数据21,那么我们是否可以修改事务隔离级别来避免并发问题呢?答案是可以的。
修改事务注解@Transactional(isolation=Isolation.READ_UNCOMMITTED)

修改完成之后,再次执行测试用例,我们可以看到超卖现象已解决。原因是因为线程2查询到的是线程1还未提交的数据20。
类似于多例模式,每一个服务中获取的锁,并不是同一个对象锁,这样自然就会产生并发问题导致多卖现象。相当于每个服务器中都会有一个stockService实例去查询和扣减库存。
sql解决超卖的方式
update insert delete写操作本身就会加锁
我们将代码改造为如下,只保留一个带有库存数量大于等于1判断的update操作。

update stock set count= count -1 where item_code = {itemCode} and count >= 1;
但是一个更新sql语句也会有一定优缺点:
(1)锁范围问题
(2)同一个商品如果有多条不同仓库库存记录
(3)先记录库存变化前后的状态
上面的update sql 锁定的时候是采用的表级锁,锁定范围过大。
mysql 悲观锁中使用行级锁:锁的查询或更新条件必须是索引字段,查询或者更新条件必须是具体值(=,in,不能是like,!= 这种查询条件)。
select * from stock where item_code = '1001' for update ;
同样和update 一样需要注意锁定范围。但同样也存在一些问题,比如处理性能下降,死锁问题等,需要对多条数据加锁时,加锁顺序要一致,库存操作要统一,比如入库,出库,扣减库存都要使用select .. for update。

compare and swap 比较并交换
例如我们可以在设计表的时候增加一列version 版本号字段或者时间戳字段来实现乐观锁。
select * from db_stock where product_code = '1001';
update db_stock set count = 4996, version = version+1 where id = 1 and version = 0;


在使用乐观锁的时候需注意两点,一是不能加事务注解,二是在更新失败的时候,防止无限的进栈出栈导致栈溢出,增加一个短暂睡眠。
乐观锁存在的问题:高并发情况下,性能极低,ABA问题,读写分离情况下导致乐观锁不可靠
性能: 一个update sql > 悲观锁 > jvm锁 > 乐观锁
如果追求极致性能,业务场景简单并且不需要记录数据前后变化的情况下,有限选择: 一个update sql。
如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁
如果写并发量较高,一般会经常冲突,此时如果选择乐观锁的话,会导致业务代码不间断的重试,优先选择: mysql悲观锁
不推荐jvm本地锁。