导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
一、问题分析
1.1 幂等性失效导致重复提交表单问题
问题:高并发下,系统负载大,采用分布式锁实现幂等性时,在解锁到提交事务期间,其他线程获取到锁并提交事务。
结果:导致同一用户插入了两条相同的数据。
伪代码模拟:
@Transactional
public void submit() {
boolean lockFlag=lock(); //例如setnx
if(!lockFlag) {
//加锁失败
throw new RuntimeException(“请稍后再试”);
}
//加锁成功
if(!dao.query()) //如果查不到数据
dao.insert(); //则插入一条数据
unlock();
//解锁后,事务还没来得及提交,此线程就阻塞了。
//此时另一个线程成功获取、查数据(数据依然不存在,因为上个线程的事务并没有提交)插数据、释放锁、提交事务。
}
//此线程执行完毕,又提交了一次事务。导致两个线程都成功插入了数据。
在事务中,使用了 Redis 分布式锁.这个方法一旦执行,事务生效,接着就 Redis 分布式锁生效,代码执行完后,先释放 Redis 分布式锁,然后再提交事务数据,最后事务结束。在这个过程中,事务没有提交之前,分布式锁已经被释放, 导致分布式锁失效。
1.2 秒杀超卖问题
案例一:
加锁{
查表
取值
更新
}
释放锁
以线程A和B为例:
- 线程A得到锁,
- 线程A查看user表得到账户余额,,
- 线程A加上前端传来的余额,
- 线程A更新数据库。
- 开启事务
- 执行更新语句(注意此时程序顺序执行释放锁,线程B获取锁)
- 线程B获取锁,
- 查询user表获得未更新前的账户余额,
- 提交事务
- 线程B加上前端传来的余额,
- 线程B更新数据库。
案例二:
@Transactional
public void seckill() {
boolean lockFlag=lock(); //例如setnx
if(!lockFlag) {
//加锁失败
throw new RuntimeException(“请稍后再试”);
}
//加锁成功
if(dao.queryStock()>0){ //如果查询库存有余额。例如余额是1
dao.updateStock(); //则减库存。此时余额是0
}
unlock();
//此时解锁成功了,因为事务还没有提交,此线程又阻塞了,此时另一个线程成功获取释放锁、查询库存是1(因为读不到未提交事务的数据),就减库存提交事务。
}
//此线程执行完毕,因为没有异常,所以又提交了一次事务,导致多卖了一次商品
二、解决方案
2.1 方案一:加唯一索引
如果是表单重复提交场景,可以尝试给“订单号”等有唯一性的字段加唯一索引,这样重复提交时会因为唯一索引约束导致索引失效。
使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的,但允许有多个空值。在一张数据表里可以有多个唯一索引。
唯一约束和唯一索引的区别:
1、唯一约束和唯一索引,都可以实现列数据的唯一,列值可以有null。
2、创建唯一约束,会自动创建一个同名的唯一索引,该索引不能单独删除,删除约束会自动删除索引。唯一约束是通过唯一索引来实现数据的唯一。
3、创建一个唯一索引,这个索引就是独立,可以单独删除。
4、如果一个列上想有约束和索引,且两者可以单独的删除。可以先建唯一索引,再建同名的唯一约束。
5、如果表的一个字段,要作为另外一个表的外键,这个字段必须有唯一约束(或是主键),如果只是有唯一索引,就会报错。
2.2 方案二:事务外层加锁
- 分布式锁在controller层中添加,事务在service层中添加。
- 使用编程式事务,外层是锁,内层是事务。
2.3 方案三:嵌套事务
将查表更新表的操作单独封装成一个方法(在事务外面加锁)。然后加上spring事务(嵌套提交)。
@Transactional
public void submit() {
if(lock()){
//提交表单的业务逻辑会生成一个嵌套事务,子事务提交回滚独立于外层事务。
xxxService.submitAfterUnLock(); //注意别用this调用,会失效。
}
unlock();
//此时另一个线程,
}
@Transactional(propagation = Propagation.NESTED) //嵌套事务
public void submitAfterUnLock() {
if(!dao.query()) insert();//如果查不到表单(通过订单号),则提交表单。
}