⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7311603432925102095 「花自飘0水自流」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、引出问题

很多小伙伴使用Spring事务时,为了省事都喜欢使用@Transactional。但是@Transactional配合锁,会导致一些预期之外的问题!

在此举例说明。

1、数据准备

我们将在该表中,实现level数据递减的并发操作。

Controller中,简单模拟10个线程各自执行10次:

二、@Transactional是如何导致锁失效的

1、不加锁

// service代码
public void test() {
// 简单的select + update 模拟业务场景
Model model = mapper.choseOne("99");

// 实现 level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
}

执行结果:我们发现,level只扣减了26,说明存在并发问题!

2、使用锁

// service代码
private Lock lock = new ReentrantLock();

public void test() {
try {
//加锁
lock.lock();
// 简单的select + update 模拟业务场景
Model model = mapper.choseOne("99");

// 实现 level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
} finally {
lock.unlock(); // 解锁
}
}

执行结果:我们发现,使用锁是可以控制并发问题。

3、使用锁+@Transactional

// service代码
private Lock lock = new ReentrantLock();

@Transactional
public void test() {
try {
//加锁
lock.lock();
// 简单的select + update 模拟业务场景
Model model = mapper.choseOne("99");

// 实现 level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
} finally {
lock.unlock(); // 解锁
}
}

执行结果:我们发现,level只扣减了86!用了@Transactional之后,锁怎么就失效了呢!

4、问题分析

我们都知道,@Transactional是通过使用AOP,在目标方法执行前后进行事务的开启和提交。所以,Lock锁住的代码,其实并没有包含住一整个事务!

通过下面的图理解一下:

当线程A将level设置为99时,此时锁已经释放了,但是事务还没提交!!线程B此时可以获取到锁并进行查询,查询出来的level还是线程A修改之前的100,所以出现了并发问题。

三、解决方案

1、@Transactional单独一个方法

private Lock lock = new ReentrantLock();
@Transactional
public void test1() {
// 简单的select + update 模拟业务场景
Model model = mapper.choseOne("99");

// 实现 level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);
}

@Autowired
@Lazy
private CommonService commonService;
public void test() {
try {
// 加锁
lock.lock();
// 自己注入自己,以使用到其代理类
commonService.test1();
} finally {
lock.unlock(); // 解锁
}
}

执行结果:没有并发问题出现!

或者直接在controller层加锁,也是一样的道理。

2、使用编程式事务

// service代码
private Lock lock = new ReentrantLock();
@Autowired
private PlatformTransactionManager transactionManager;
public void test() {
try {
//加锁
lock.lock();
// 编程式事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

// 简单的select + update 模拟业务场景
Model model = mapper.choseOne("99");

// 实现 level -- 操作
Model updater = new Model();
updater.setId("99");
updater.setLevel(model.getLevel() - 1);
mapper.updateOne(updater);

// 在锁中提交
transactionManager.commit(status);
} finally {
lock.unlock(); // 解锁
}
}

执行结果:我们发现,将整个事务都锁住,就没问题了!

文章目录
  1. 1. 一、引出问题
    1. 1.1. 1、数据准备
  2. 2. 二、@Transactional是如何导致锁失效的
    1. 2.1. 1、不加锁
    2. 2.2. 2、使用锁
    3. 2.3. 3、使用锁+@Transactional
    4. 2.4. 4、问题分析
  3. 3. 三、解决方案
    1. 3.1. 1、@Transactional单独一个方法
    2. 3.2. 2、使用编程式事务