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

简介: 在事务中,使用了 Redis 分布式锁.这个方法一旦执行,事务生效,接着就 Redis 分布式锁生效,代码执行完后,先释放 Redis 分布式锁,然后再提交事务数据,最后事务结束。如果是表单重复提交场景,可以尝试给“订单号”等有唯一性的字段加唯一索引,这样重复提交时会因为唯一索引约束导致索引失效。5、如果表的一个字段,要作为另外一个表的外键,这个字段必须有唯一约束(或是主键),如果只是有唯一索引,就会报错。2、创建唯一约束,会自动创建一个同名的唯一索引,该索引不能单独删除,删除约束会自动删除索引。

 导航:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析

目录

一、问题分析

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();
//解锁后,事务还没来得及提交,此线程就阻塞了。
//此时另一个线程成功获取、查数据(数据依然不存在,因为上个线程的事务并没有提交)插数据、释放锁、提交事务。
}
//此线程执行完毕,又提交了一次事务。导致两个线程都成功插入了数据。

image.gif

在事务中,使用了 Redis 分布式锁.这个方法一旦执行,事务生效,接着就 Redis 分布式锁生效,代码执行完后,先释放 Redis 分布式锁,然后再提交事务数据,最后事务结束。在这个过程中,事务没有提交之前,分布式锁已经被释放, 导致分布式锁失效。

1.2 秒杀超卖问题

案例一:

加锁{
    查表
    取值
    更新
}
释放锁

image.gif

以线程A和B为例:

  1. 线程A得到锁,
  2. 线程A查看user表得到账户余额,,
  3. 线程A加上前端传来的余额,
  4. 线程A更新数据库。
  • 开启事务
  • 执行更新语句(注意此时程序顺序执行释放锁,线程B获取锁
  • 线程B获取锁,
  • 查询user表获得未更新前的账户余额,
  • 提交事务
  1. 线程B加上前端传来的余额,
  2. 线程B更新数据库。

案例二:

@Transactional
public void seckill() {
    boolean lockFlag=lock();    //例如setnx
    if(!lockFlag) {
//加锁失败
        throw new RuntimeException(“请稍后再试”);
    }
//加锁成功
    if(dao.queryStock()>0){    //如果查询库存有余额。例如余额是1
        dao.updateStock();    //则减库存。此时余额是0
    }
    unlock();
    //此时解锁成功了,因为事务还没有提交,此线程又阻塞了,此时另一个线程成功获取释放锁、查询库存是1(因为读不到未提交事务的数据),就减库存提交事务。
}
//此线程执行完毕,因为没有异常,所以又提交了一次事务,导致多卖了一次商品

image.gif

二、解决方案

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();//如果查不到表单(通过订单号),则提交表单。
}

image.gif


相关文章
|
6月前
|
缓存 数据库
并发修改同一记录时需要加锁
并发修改同一记录时需要加锁
|
3月前
|
索引 关系型数据库 MySQL
锁与索引和释放锁时机
【8月更文挑战第1天】
48 1
|
6月前
|
数据库连接 数据库
多线程事务失效的原因
【5月更文挑战第16天】多线程事务失效的原因
384 0
|
6月前
|
关系型数据库 MySQL 数据库
事务和锁:保证数据一致性
事务和锁:保证数据一致性
69 0
|
关系型数据库 MySQL 数据库
并发事务更新问题
并发事务更新问题
60 0
|
前端开发 关系型数据库 MySQL
用户重复注册分析-多线程事务中加锁引发的bug
用户重复注册分析-多线程事务中加锁引发的bug
142 0
并发锁(一):为什么要加锁
并发锁(一):为什么要加锁
162 0
并发锁(一):为什么要加锁
|
监控 NoSQL Redis
事务-锁|学习笔记
快速学习事务-锁