1. 前言
前两天,有个群友遇到个问题,明明加了 synchronized 关键字,为什么就不起作用呢?锁怎么就失效了呢?然后我看了他发给我的代码。
大致需求是这样的,客户表中类型 (type) 为 1 的手机号不能重复,但是其他类型的手机号可以重复,这就意味着 mobile 是不能加唯一索引的,mobile 和 type 也不能加联合唯一索引。
在插入客户时需要根据 mobile 和 type 为 1 来判断记录是否存在,为了防止并发问题在方法上加了 synchronized 关键字。而在插入成功后还要同时操作其他数据表,所以方法上加上了 @Transactional 事务注解。伪代码看起来就是下面这样子。
1 |
|
看起来是不是考虑很周全,很完美?其实我看的 Transactional 注解之后,我已经知道是什么原因了。
2. 为什么synchronized遇到@Transactional会失效?
2.1 synchronized锁失效
这个得从 @Transactional 说起,我们都知道 Transactional 的实现是基于 Spring 的 AOP 。Spring 会对 addCustomer 方法进行动态代理,执行前开始事务,然后执行 addCustomer 方法的业务逻辑,最后提交事务。而执行 addCustomer 方法的时候,先加锁,执行逻辑,再解锁。最后的大致流程图如下:
假设线程1先抢到锁,在解锁之后,这个时候事务还没有提交,数据库数据还没有更新。然后线程2获取到了锁,执行逻辑,执行业务逻辑的时候很有可能获取的还是旧数据,出现锁失效的情况。
2.2 分布式锁失效
大多数情况下,我们都是分布式系统,会使用分布式锁。比如使用 ZooKeeper 或者 Redis 实现的分布式锁。不熟悉分布式锁的同学可以看这几篇文章—— 。
此时代码会变成这样:
1 |
|
其实,效果是一样的,无论使用 synchronized 还是分布式锁,最终导致锁失效的本质原因是解锁在提交事务之前。
3. 解决方案
第一种方案就是在 Controller 层加锁了,类似于下面这样。
1 | synchronized (this) { |
第二种方案是在 Service 层里面再写一个方法,类似于下面这样。
1 |
|
总结:
- 在实际开发过程中,要确认好是否需要使用 @Transactional 进行事务管理,不要瞎用。
- 在需要加锁并且需要保证事务的场景中,尤其需要注意锁是否在事务提交之前释放。