一 方案1 使用悲观锁解决冲突
1.1 使用悲观锁原理
1.1.1 使用悲观锁的原理
1.悲观锁:在select的时候就会加锁,采用先加锁后处理的模式,虽然保证了数据处理的安全性,但也会阻塞其他线程的写操作。在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。select ... for update
悲观锁适用于写多读少的场景,因为拿不到锁的线程,会将线程挂起,交出CPU资源,可以把CPU给其他线程使用,提高了CPU的利用率。
1.1.2 使用悲观锁的优缺点
优点: 1.简单容易理解;2.可以严格保证数据访问的安全;
缺点:
1.即每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。另外,悲观锁使用不当还可能产生死锁的情况。
2.性能一般。
1.1.3 使用悲观锁的使用行锁&表锁
1.使用悲观锁时,查询条件必须添加索引,成为索引字段,才走行锁。
2.使用悲观锁是,查询条件不加索引,走表锁。
1.1.4 演示案例
1会话A未提交状态
在会话A中: 执行命令: begin;select * from db_stock where product_code='1001' for update
会话A:select ... for update 给具体的行数据加上排他锁(product_code加上索引),也即行锁。
会话B :无法对1001进行更新,因为上了行级锁,无法进行更新
2会话A提交状态
会话B:进行了修改
1.2 操作案例
使用sql语句: select ... for update 给具体的行数据加上排他锁(product_code加上索引),也即行锁。
1.mapper:编写悲观锁语句
2.service:添加事务注解 @Transactional
3.数据表
4.jmeter压力测试
5.查看效果:成功实现所减数据为0,均正确消费。
1.3 死锁场景模拟
1.表数据
2.A会话
3.B会话
说明:A会话中,先锁住id=1,再锁住id=2;B会话中,先锁住id=2,再锁住id=1;彼此等待获取锁。则会造成死锁
1.4 此方案的优缺点
1.性能问题;2.死锁问题:对多条数据加锁时,加锁顺序要一致;
3.库存操作要统一,一个会话用 select x for update 一个会话执行select可以进行查询 ,可能存在数据不一致情况。
会话A:进行查询上行锁,处于未commit状态时,会话B进行 select 查询,会话B可以查询出内容。
二 方案2:使用乐观锁解决冲突
2.1 乐观锁原理
乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。适合于读比较多,不会阻塞读,读取数据时不上锁,更新时检查是否数据已经被更新过,如果是则取消当前更新进行重试。version 或者 时间戳(CAS思想)。
2.2 操作案例
使用数据版本(Version)记录机制实现,这是乐观锁最常用的实现 方式。一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录 的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新。
更新sql:
select * from db_stock where product_code='1001'
update db_stock set count=4996,version=version+1 where id=1 and version=0;
1.修改service
2.数据库表
3.压力测试
4.查看消费结果: 均正确消费
2.3 乐观锁存在的缺点
优点:
优点比较明显,由于在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能,当产生并发且并发量较小的时候只有少部分请求会失败。
缺点:
1.缺点是需要对表的设计增加额外的字段,增加了数据库的冗余,
2.另外,当应用并发量高的时候,version值在频繁变化,则会导致大量请求失败,影响系统的可用性。
1.高并发情况下,性能比较低下,并发量越小,性能越高。
2.读写情况下,乐观锁不可靠。
三 利用unique唯一键索引实现解决冲突
3.1 原理
可以利用唯一键索引不能重复插入的特点实现。
3.2 流程图
-
线程同时获取锁(insert)
-
获取成功,执行业务逻辑,执行完成释放锁(delete)
-
其他线程等待重试
3.3 核心代码
3.4 方案的优缺点
-
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
解决方案:给 锁数据库 搭建主备
-
这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
解决方案:只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
-
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。
-
受制于数据库性能,并发能力有限。
解决方案:无法解决。