赞
踩
很多小伙伴使用Spring事务时,为了省事都喜欢使用@Transactional。但是@Transactional配合锁,会导致一些预期之外的问题!
在此举例说明。
针对下文用例,定义:
数据库表:
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(16) COLLATE utf8mb4_bin NOT NULL COMMENT 'name',
`age` int NOT NULL COMMENT 'age',
`level` int NOT NULL COMMENT 'level',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'
INSERT INTO `user` (`id`, `name`, `age`, `level`) VALUES ('1','Tom','19','100');
实体类:
public class User{
private Long id;
private String name;
private Integer age;
private Integer level;
// 略get/set方法
}
Service层代码
@Service
public class UserService {
@Autowired
private UserMapper mapper;
public void getAndReduceLevel() {
User user = mapper.selectById(1L);
User updateDTO = new User();
updateDTO.setId(1L);
updateDTO.setLevel(user.getLevel()-1);
mapper.updateById(updateDTO);
}
}
测试用例:
@RunWith(SpringRunner.class) @SpringBootTest(classes = {CustomerApplication.class}) public class ConcurrencyTest { @Autowired private UserService userService; @Test public void concurrencyTest() { for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) { userService.getAndReduceLevel(); } }).start(); } } }
输出结果:
执行结果:我们发现,level只扣减了16,说明存在并发问题!
@Service public class UserService { private Lock lock = new ReentrantLock(); @Autowired private UserMapper mapper; public void getAndReduceLevel() { try{ // 加锁 lock.lock(); User user = mapper.selectById(1L); User updateDTO = new User(); updateDTO.setId(1L); updateDTO.setLevel(user.getLevel()-1); mapper.updateById(updateDTO); } finally { lock.unlock(); } } }
输出结果:
通过执行结果,我们发现,使用锁是可以控制并发问题。
@Service public class UserService { private Lock lock = new ReentrantLock(); @Autowired private UserMapper mapper; @Transactional public void getAndReduceLevel() { try{ // 加锁 lock.lock(); User user = mapper.selectById(1L); User updateDTO = new User(); updateDTO.setId(1L); updateDTO.setLevel(user.getLevel()-1); mapper.updateById(updateDTO); } finally { lock.unlock(); } } }
输出结果:
通过执行结果,我们发现,level只扣减了54!用了@Transactional之后,锁怎么就失效了呢!
我们都知道,@Transactional
是通过使用AOP,在目标方法执行前后进行事务的开启和提交。所以,Lock锁住的代码,其实并没有包含住一整个事务!
通过下面的图理解一下:
当线程A将level设置为99时,此时锁已经释放了,但是事务还没提交!!线程B此时可以获取到锁并进行查询,查询出来的level还是线程A修改之前的100,所以出现了并发问题。
public void lockMethod() { try { // 加锁 lock.lock(); UserService userService = (UserService) AopContext.currentProxy(); userService.getAndReduceLevel(); } finally { lock.unlock(); } } @Transactional public void getAndReduceLevel() { User user = mapper.selectById(1L); User updateDTO = new User(); updateDTO.setId(1L); updateDTO.setLevel(user.getLevel()-1); mapper.updateById(updateDTO); }
输出结果:
没有并发问题出现!
使用编程式事务
业务层代码
private Lock lock = new ReentrantLock(); @Autowired private PlatformTransactionManager transactionManager; public void getAndReduceLevel() { try { // 加锁 lock.lock(); TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); User user = mapper.selectById(1L); User updateDTO = new User(); updateDTO.setId(1L); updateDTO.setLevel(user.getLevel()-1); mapper.updateById(updateDTO); transactionManager.commit(status); } finally { lock.unlock(); } }
输出结果:
编程式事务也能锁住了。
在@Transactional注解中使用线程锁(例如synchronized关键字或ReentrantLock)确实可能导致一些与事务和锁相关的行为不如预期。这种情况主要出现在多个线程或事务并发访问共享资源时。
锁与事务的粒度不匹配,线程锁通常用于同步访问某个对象的临界区,确保同一时间只有一个线程能够执行这段代码。而事务则是用于确保数据库操作的原子性、一致性、隔离性和持久性。如果锁的粒度太粗,可能会阻止其他线程进行无关的事务操作,导致性能下降。如果锁的粒度太细,又可能无法正确同步关键的业务逻辑。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。