问题分析

我的代码逻辑如下:

@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED)
public synchronized boolean buy(Integer id) {boolean b = false;int stock = mapper.getStock(id);if (stock > 0) {System.out.println("库存为:" + stock);b = mapper.updateStock(id, stock - 1);}return b;
}

虽然该方法加了锁,看似线程安全、人畜无害,但结果还是有可能会超卖,为啥呢?先来看看@Transactional注解的奥秘。

PlatformTransactionManager是spring处理事务的核心规范,它是一个接口:

public interface PlatformTransactionManager extends TransactionManager {TransactionStatus getTransaction(@Nullable TransactionDefinition definition)throws TransactionException;void commit(TransactionStatus status) throws TransactionException;void rollback(TransactionStatus status) throws TransactionException;}   

从该接口中可以看到如下:

  • TransactionDefinition:该接口里面规范了事务的隔离级别、传播级别、是否只读、超时时间等定义信息
  • TransactionStatus:可以理解为就是一个事务,通过它可以获取事务的状态
  • getTransaction()方法:根据TransactionDefinition获取一个事务对象
  • commit():提交事务
  • rollback():回滚事务

PlatformTransactionManager的实现类如下:

其中的DataSourceTransactionManager比较常用。

在PlatformTransactionManager上的getTransaction()打上断点,请求我们的方法:

  1. 进入AbstractPlatformTransactionManager类的getTransaction()上,@Transaction注解上设置的事务配置信息就是它传过来的参数,然后去校验配置、根据事务的隔离级别选择是否创建事务。

  2. 如果需要事务,则进入子类DataSourceTransactionManager的doBegin()方法,关键点。该方法首先会根据数据源获取数据库的connection,然后针对当前获取的connection,将当前会话的的事务开启方式改为手动提交

注意此时事务还并未开始噢,还需要手动执行begin和start transaction这两个命令,才算开启事务

来验证一下:

-- 查看当前数据库有哪些事务存在
select * from information_schema.innodb_trx;

得到的结果为null,没有事务开启。

再看如下调用栈:

我们之前的入口方法getTransaction()其实是TransactionAspectSupportinvokeWithinTransaction()方法调用的。方法如下:

这里有个切面,可以理解为 try 里面就是在执行我们的业务代码逻辑,而try前面的create..方法就是准备好事务,时机成熟后就开启事务。

什么时候时机成熟了?请看下文

经过一定的步骤,我们从切面跑到了我们原本的代码逻辑,准备开始执行业务了。

此时还没有事务信息

继续走,当执行完数据的查询操作后,即涉及到数据库的语句后事务就开起来了

把我们的业务逻辑执行完后,回到刚刚的切面

1.顺着completeTransactionAfterThrowing()方法走,你会发现spring事务的默认回滚的异常是RuntimeException或者Error。使用instanceof判断的。

2.finally块中的cleanupTransactionInfo()方法并不是提交事务,而是恢复事务的默认行为(隔离级别、回滚类型等)。

3.commitTransactionAfterReturning()提交事务的方法不是一定提交,如果判断事务配置为只读,那么就会回滚。

由此我们就知道了spring事务的一个大致过程:

  1. 先设置事务的开启方式为手动
  2. 执行业务代码
  3. 涉及到数据库的增删改查操作时就立即开启一个事务
  4. 我们的业务代码执行完毕
  5. 如果中途有异常则回滚事务
  6. 否则,默认提交事务

最后可以分析出我们的业务逻辑中,获取锁的步骤是在开启事务之前,释放锁的操作也在提交事务之前

这就出现了一个问题,在释放锁和提交事务这一小块区间可能会引发线程安全问题。

比如:

线程A扣减库存为0了,然后释放锁,还没来得及提交事务,此时线程B突然冲过来,获取到锁,然后开启事务,查询库存,因为是不可重复读,所以线程B是读取不到线程A的修改的,它读取到的库存依旧充足,所以线程B也扣减库存,到最后也就超卖了。

解决方法

现在我们要避免之前的错误,正确的使用锁,把整个事务放在锁的工作范围之内:

//controller调用该方法,该方法间接的去调用我们的业务逻辑
public boolean director(Integer id){synchronized (this){return buy(id);}
}@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {boolean b = false;int stock = mapper.getStock(id);if (stock > 0) {System.out.println("库存为:" + stock);b = mapper.updateStock(id, stock - 1);}return b;
}

这样,就可以保证事务的提交一定是在 unlock 之前了。

no no no!

这样做,事务并不会生效。

如果此时事务能生效就可以保证这段代码是线程安全的,不会出现超卖问题。关于事务失效请看下文的解决方案:

事务的失效场景

CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的方法。在子类中采用方法拦截的技术拦截(MethodInterceptor类)所有父类方法的调用,顺势织入横切逻辑。

可参考:【动态代理】CGLIB 动态代理的使用及原理_sco5282的博客-CSDN博客_cglib动态代理使用

  • 方法不是public权限修饰的,spring事务默认生效的方法权限都必须为public。

解决方案:1、将方法改为public; 2、修改TansactionAttributeSource,将publicMethodsOnly改为false;3、开启 AspectJ 代理模式

  • 方法是final修饰的,final方法不能被重写

失效原因: 因为spring事务是用动态代理实现的,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能

  • 方法是static

失效原因: 原因和final一样

  • 数据库的存储引擎本身不支持事务,如MyISM。

解决方案:使用InnoDB引擎

  • Service类没有被spring管理,没写@Service、@Component等注解。
  • 异常被捕获,没有抛出方法外,该事务不会回滚。

解决方案:1、将异常原样抛出; 2、设置TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

  • 异常类型和事务的rollbackFor不匹配,默认回滚的是RuntimeException和Error错误

解决方案:配置rollbackFor

  • 调用自身方法
public boolean director(Integer id) {synchronized (this) {return this.buy(id);  //或return buy(id);}
}@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {//省略
}

失效原因: Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。

解决方案:

1、注入自己来调用:

@Autowired
@Lazy //防止循环依赖
private ProductService service;public boolean director(Integer id) {synchronized (this) {//不能使用this.调用return service.buy(id);}
}@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {//省略
}

2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy(),通过获取代理对象调用

步骤:引入aspectjweaver依赖、启动类加@EnableAspectJAutoProxy(exposeProxy = true),暴露代理对象、获取当前代理对象调用

public boolean director(Integer id) {synchronized (this) {//获取当前代理类调用方法return ((ProductServiceImpl)AopContext.currentProxy()).buy(id);}
}@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {//省略
}
  • 错误的使用事务的传播机制,也会导致事务失效

顺便说下吧,看下文

事务的传播机制

事务传播机制主要用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的事务中,该事务如何传播。这个概述可能不好理解,换句话就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

@Transaction(Propagation=XXX)
public void methodA(){methodB();//doSomething}@Transaction(Propagation=XXX)public void methodB(){//doSomething}

全部的传播机制:

事务传播行为类型 解释说明
Propagation_Required 表示被修饰的方法必须运行在事务中。如果当前方法没有事务,则就新建一个事务;如果已经存在一个事务中,就加入到这个事务中。此类型是最常见的默认选择
Propagation_Supports 表示被修饰的方法不需要事务上下文。如果当前方法存在事务,则支持当前事务执行;如果当前没有事务,就以非事务方式执行。
Propagation_Mandatory 表示被修饰的方法必须在事务中运行。如果当前事务不存在,则会抛出一个异常。
Propagation_Required_New 表示被修饰的方法必须运行在它自己的事务中。一个新的事务会被启动。如果调用者存在当前事务,则在该方法执行期间,当前事务会被挂起。
Propagation_Not_Supported 表示被修饰的方法不应该运行在事务中。如果调用者存在当前事务,则该方法运行期间,当前事务将被挂起。
Propagation_Never 表示被修饰的方法不应该运行事务上下文中。如果调用者或者该方法中存在一个事务正在运行,则会抛出异常。
Propagation_Nested 表示当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立与当前事务进行单独地提交或者回滚。如果当前事务不存在,那么其行为与Propagation_Required一样。

嵌套事务的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

解决问题

有了上面对spring事务的分析,我们得出最后的解决超卖问题的方法为:

public boolean director(Integer id) {synchronized (this) {return ((ProductService)AopContext.currentProxy()).buy(id);}
}@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {boolean b = false;int stock = mapper.getStock(id);if (stock > 0) {System.out.println("库存为:" + stock);b = mapper.updateStock(id, stock - 1);}return b;
}

spring事务出现的超卖问题相关推荐

  1. Spring Boot + redis解决商品秒杀库存超卖,看这篇文章就够了

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:涛哥谈篮球 来源:toutiao.com/i68366119 ...

  2. 带你读懂Spring 事务——事务的隔离级别(超详细,快藏)

    不了解事务的铁汁可以先看前两篇,讲的超详细,有问题还请您指点一二 带你读懂Spring 事务--认识事务 带你读懂Spring 事务--事务的传播机制(藏) 特别提示:本文所进行的实验都是在MySQL ...

  3. 事务隔离机制原理分析以及是否可以防止订单超卖

    事务的隔离机制是指: Read Uncommitted(读取未提交内容) Read Committed(读取提交内容) Repeatable Read(可重读) Serializable(可串行化) ...

  4. 秒杀 mysql 事务_秒杀怎么样才可以防止超卖?基于mysql的事务和锁实现

    Reference:  http://blog.ruaby.com/?p=256 并发事务处理带来的问题? 相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从 ...

  5. Redis 事务机制实现过程及原理,以及使用事务机制防止库存超卖

    一.Redis 事务实现的过程和原理 第一步:观察数据 客户端在修改数据之前,先使用watch命令观察要修改的数据,这一步相当于记下了数据的版本号 第二步:开启事务 使用 multi 命令开启事务. ...

  6. 教你从0到1搭建秒杀系统-防超卖

    各位读者好,最近笔者学了很多东西,其实都想跟大家进行分享,奈何需要将所学习的知识整理出来需要耗费大量的时间,包括总结,或各种图形以及写代码示例,所以可能更新的速度会比较慢.但大家放心,只要有时间我就会 ...

  7. redis decr 防止超卖_Redis基础、高级特性与性能调优——一篇文章搞定

    本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍.之后概览Redis提供的高级能力,并在部署.维护.性能调优等多个方面进行更深入的介绍和指导. ...

  8. java 超卖_Java生鲜电商平台-秒杀系统如何防止超买与超卖?(小程序/APP)

    本文主要是通过实际代码讲解,帮助你一步步搭建一个简易的秒杀系统.从而快速的了解生鲜电商秒杀系统的主要难点,并且迅速上手实际项目. 我对生鲜电商秒杀系统文章的规划: 从零开始打造简易秒杀系统:乐观锁防止 ...

  9. 【秒杀系统】从零开始打造简易秒杀系统(一):防止超卖

    前言 大家好,好久不发文章了.(快一个月了- -)最近有很多学习的新知识想和大家分享,但无奈最近项目蛮忙的,很多文章写了一半搁置在了笔记里,待以后慢慢补充发布. 本文主要是通过实际代码讲解,帮助你一步 ...

  10. 阿里面试官:高并发大流量秒杀系统如何正确的解决库存超卖问题?(建议收藏)

    大家好,我是冰河~~ 在[精通高并发系列]的<实践出真知:全网最强秒杀系统架构解密!!>一文中,冰河详细的阐述了高并发秒杀系统的架构设计,也简单提到了如何扣减商品的库存. 也许不少小伙伴会 ...

最新文章

  1. 机械转行的都干啥去了?机械转行计算机难吗?
  2. Linux系统学习笔记:文件描述符标志
  3. 安徽省计算机一级文化基础,计算机一级文化基础选择题
  4. 提高使用比特币的匿名性
  5. python 类继承和组合_Python:继承与组合
  6. python 清空所有对象_Python编程思想(7):列表的增删改操作
  7. Java语言学校的危险性(译文)
  8. 小技巧来助阵 玩转Chrome浏览器
  9. nginx与PHP配置
  10. vue element table
  11. 前端小白该如何学习?前端开发工程师掌握哪些能力
  12. Docker详解(十五)——Docker静态IP地址配置
  13. 在Ubuntu上搭建Hadoop群集
  14. cgi、fast-cgi、php-pfm关系
  15. 微信小程序人脸识别认证-微信开放接口
  16. 【神器】yololib 飘云修改版 -- 给iOS APP 添加导入表注入--你懂的
  17. python列表获取最后一项_如何在Python中获取列表的最后一项?
  18. ios python 越狱_如何使用Frida绕过iOS应用程序中的越狱检测!!!
  19. api-gateway的pom文件
  20. 华为防火墙VRRP双机热备的配置

热门文章

  1. 翟菜花:四家电商平台Q3财报梳理:涨幅狂欢后的沉思
  2. 翟菜花:搭上营销快通车的乳业,又是如何玩转互联网营销时代的?
  3. 群晖NAS使用Docker安装迅雷离线下载出现the active key is not valid.
  4. 设置个性桌面计算机教案,第十三课《个性化的桌面设置》教学设计
  5. 消费者人群画像-信用智能评分(金融风控模型经典案例)
  6. odi oracle to mysql_【ODI】| 数据ETL:从零开始使用Oracle ODI完成数据集成(三)
  7. Qcon演讲纪实:详解如何在实时视频通话中实现AR功能
  8. ES数据的操作(二)
  9. Vue中相同逻辑如何抽离?
  10. 运动型蓝牙耳机什么牌子好、最适合运动的耳机