【幂等性大坑】事务提交前释放锁导致锁失效问题

导航:

【Java笔记+踩坑汇总】Java基础+进阶+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+MySQL高级篇+设计模式+常见面试题+源码

目录

一、问题分析

1.1 幂等性失效导致重复提交表单问题

1.2 秒杀超卖问题

二、解决方案

2.1 方案一:加唯一索引

2.2 方案二:事务外层加锁

2.3 方案三:嵌套事务


一、问题分析

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为例:

  1. 线程A得到锁,
  2. 线程A查看user表得到账户余额,,
  3. 线程A加上前端传来的余额,
  4. 线程A更新数据库。
    • 开启事务
    • 执行更新语句(注意此时程序顺序执行释放锁,线程B获取锁
      • 线程B获取锁,
      • 查询user表获得未更新前的账户余额,
    • 提交事务
  5. 线程B加上前端传来的余额,
  6. 线程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();//如果查不到表单(通过订单号),则提交表单。

}

你可能感兴趣的:(Java学习路线,分布式,java,spring,spring,boot,spring,cloud)