起因:
前两天看到一段代码,分布式锁于@Transactional组合使用导致接口最终结果有误,趁今天周末有空我们写个简单的demo来复现一下,这不是该放中秋节抢票了吗,那咱就来模拟下抢票呗。
controller层代码
import com.syl.cloud_2024.service.TransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class TransactionalController {
@Autowired
private TransactionalService transactionalService;
@GetMapping("testA")
public void testA() {
// 模拟50个人买票
for (int i = 1; i <= 50; i++){
new Thread() {
@Override
public void run() {
transactionalService.test();
}
}.start();
}
}
}
service层代码
public interface TransactionalService {
/**
*
* @author yeYingXuan
*/
void test();
}
service实现层代码
@Service
@Slf4j
public class TransactionalServiceImpl implements TransactionalService {
@Autowired
FarmService farmService;
@Autowired
RedisLockService redisLockService;
@Autowired
private TestAMapper testAMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
String lockKey = "lock:Api:test2";
try {
// 加锁并设置过期时间3秒,等待时常3秒
boolean locked = redisLockService.tryLock(lockKey, Thread.currentThread().getName(), 3, TimeUnit.SECONDS, 3000);
if (locked) {
TestA oldTestA = testAMapper.selectById(1);
int level = oldTestA.getLevel() - 1;
log.info("我是线程{},我买完还剩余{}张高铁票", Thread.currentThread().getName(), level);
TestA testA = new TestA();
testA.setId(1);
testA.setLevel(level);
testAMapper.updateById(testA);
} else {
throw new RuntimeException("稍后再试");
}
} finally {
// 释放锁(只有当前线程持有锁时才释放锁)
redisLockService.unlock(lockKey, Thread.currentThread().getName());
}
}
}
准备数据,一共50张票,50个人买完看看还剩余多少。
结果明显不对,50个人,50张票,卖完了怎么还剩余2张票呢?我们再来看看买票日志
看到结果后明显有2张票卖重复了,脑补下画面(这4个人上车后扭打一起。。。)
到此处我们已经复现出来了问题,接着就是分析一下问题发生原因,以及怎么解决。
分析原因:
乍看一下代码貌似没问题,有redis锁控制并发,有事务控制数量,事务隔离级别为读已提交,保证查到的剩余数量一定是上一个人买成功后的数量。但是问题就出现在是因为在事务内加的锁与释放锁。
我们都知道,@Transactional 是 Spring 框架中用于声明式事务管理的一个注解。它的实现机制主要基于 AOP(面向切面编程)和动态代理。而进入到test()前会代理类会开启事务,test()方法走完后会走代理类会走提交事务方法。所以问题原因就是test()执行完,锁已经释放了,而事务还没提交。接下来我们验证一下
验证:
我们在DynamicAdvisedInterceptor类的intercept方法打上断点
中间断点省略。。。。。
最终在TransactionAspectSupport类中的invokeWithinTransaction方法中看到了我们熟悉的代码
事实证明确实是test()方法执行完后锁释放了,但是事务没有提交导致下一个线程买票时候重复了。
解决问题:
知道了问题是怎么发生的,那就好解决了,把锁放到事务提交后执行不就ok了
controller层代码
import com.syl.cloud_2024.service.TransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class TransactionalController {
@Autowired
private TransactionalService transactionalService;
@GetMapping("testA")
public void testA() {
for (int i = 1; i <= 50; i++){
new Thread() {
@Override
public void run() {
transactionalService.test2();
}
}.start();
}
}
}
service层代码
public interface TransactionalService {
/**
*
* @author yeYingXuan
*/
void test();
/**
*
* @author yeYingXuan
*/
void test2();
}
service层实现代码
import com.syl.cloud_2024.bean.entity.TestA;
import com.syl.cloud_2024.mapper.TestAMapper;
import com.syl.cloud_2024.service.FarmService;
import com.syl.cloud_2024.service.TransactionalService;
import com.syl.cloud_2024.utils.RedisLockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class TransactionalServiceImpl implements TransactionalService {
@Autowired
FarmService farmService;
@Autowired
RedisLockService redisLockService;
@Lazy
@Autowired
private TransactionalService transactionalService;
@Autowired
private TestAMapper testAMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
String lockKey = "lock:Api:test2";
// 加锁并设置过期时间3秒,等待时常3秒
boolean locked = redisLockService.tryLock(lockKey, Thread.currentThread().getName(), 5, TimeUnit.SECONDS, 5000);
TestA oldTestA = testAMapper.selectById(1);
int level = oldTestA.getLevel() - 1;
log.info("我是线程{},我买完还剩余{}张高铁票", Thread.currentThread().getName(), level);
TestA testA = new TestA();
testA.setId(1);
testA.setLevel(level);
testAMapper.updateById(testA);
}
@Override
public void test2() {
String lockKey = "lock:Api:test2";
try {
// 加锁并设置过期时间3秒,等待时常3秒
boolean locked = redisLockService.tryLock(lockKey, "locked", 3, TimeUnit.SECONDS, 3000);
if (locked) {
transactionalService.test();
} else {
throw new RuntimeException("稍后再试");
}
} finally {
// 释放锁(只有当前线程持有锁时才释放锁)
redisLockService.unlock(lockKey, "locked");
}
}
}
但是要注意一个问题,test2()方法不能加事务,如果加事务的话跟test()结果一样了,并且因为是本类调用,this不会走代理,所以这里自己注入一下自己。
验证结果:
结果正常。
两个小知识点:
1、@Transactional尽量和锁不要出现在同一方法。
2、调本地this方法不会走代理。