Coding & Life

求知若饥,虚心若愚

0%

Transactional注解导致锁失效的问题

在工作实践中发现,很多人在Spring项目开发中,如果@Transactional注解结合同步锁或者分布式锁时,经常会犯错,今天在此记录问题,分析原因同时给出解决方案

错误案例

数据库准备

新建一个sys_user表,同时把username字段设为唯一索引

业务逻辑

新建一个接口test,在接口上添加@Transactional注解,同时方法内添加一个synchronized同步锁。方法实现的是查询usernamea的用户是否存在,如果不存在插入一条usernamea的记录

测试问题

我们使用siege(一款压测工具)模拟并发请求

通过压测,发现其中有1个请求失败,此时观察java控制台输出


输出显示出现唯一索引冲突的问题导致插入失败。我们明明已经为方法整体增加了同步锁,为什么还会导致相同username执行插入的逻辑呢?

此处就是许多开发者经常犯的错误

原因分析

此时首先需要了解你当前使用数据库的事务隔离级别

我这里使用的是RR级别,如果有对事务隔离不清楚的可以查看我这篇文章

@Transactional是使用代理机制实现的数据库事务,有时间我会单独讲一下它的原理。它会在请求进入方法之前开启事务,同时方法结束之后提交事务(或者发生异常时回滚)

此案例中,当两个请求同时打来,会各自开启数据库事务,同时只有1个请求获得锁,执行锁内代码。由于锁内代码是包含在事务之内的,所以当执行完锁内代码,释放锁之后,另一个请求就可以获得锁,执行锁内代码。此时由于数据库事务的隔离性,第2个请求并不能查到第1个请求新增的数据(无论第1个请求是否进行了事务提交),所以它认为数据库并没有usernamea的数据,再次执行插入时,由于数据库的唯一索引,导致程序报错

解决方案

既然是锁包含在事务之内导致的问题,那我们就把事务包含在锁内就能解决这个问题,代码如下

我们抽象出一个addUser方法,该方法使用@Transactional注解。在调用该方法时,使用锁包围

通过多次测试,没有出现唯一索引冲突的问题

总结

@Transactional和同步锁一起使用时,一定要将事务的开启和关闭包含在锁内