Fork me on GitHub

Synchronized失效

1. 前言

前两天,有个群友遇到个问题,明明加了 synchronized 关键字,为什么就不起作用呢?锁怎么就失效了呢?然后我看了他发给我的代码。

大致需求是这样的,客户表中类型 (type) 为 1 的手机号不能重复,但是其他类型的手机号可以重复,这就意味着 mobile 是不能加唯一索引的,mobile 和 type 也不能加联合唯一索引。

在插入客户时需要根据 mobile 和 type 为 1 来判断记录是否存在,为了防止并发问题在方法上加了 synchronized 关键字。而在插入成功后还要同时操作其他数据表,所以方法上加上了 @Transactional 事务注解。伪代码看起来就是下面这样子。

1
2
3
4
5
6
7
8
9
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized Result<Object> addCustomer(InsertCustomerReq req) {
Customer customer = dao.getCustomerByMobileAndTypeOne();
if(null != customer){
dao.insert();
dao.insertOtherData();
}
}

看起来是不是考虑很周全,很完美?其实我看的 Transactional 注解之后,我已经知道是什么原因了。

2. 为什么synchronized遇到@Transactional会失效?

2.1 synchronized锁失效

这个得从 @Transactional 说起,我们都知道 Transactional 的实现是基于 Spring 的 AOP 。Spring 会对 addCustomer 方法进行动态代理,执行前开始事务,然后执行 addCustomer 方法的业务逻辑,最后提交事务。而执行 addCustomer 方法的时候,先加锁,执行逻辑,再解锁。最后的大致流程图如下:

假设线程1先抢到锁,在解锁之后,这个时候事务还没有提交,数据库数据还没有更新。然后线程2获取到了锁,执行逻辑,执行业务逻辑的时候很有可能获取的还是旧数据,出现锁失效的情况。

2.2 分布式锁失效

大多数情况下,我们都是分布式系统,会使用分布式锁。比如使用 ZooKeeper 或者 Redis 实现的分布式锁。不熟悉分布式锁的同学可以看这几篇文章—— 。

此时代码会变成这样:

1
2
3
4
5
6
7
8
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Object> addCustomer(InsertCustomerReq req) {
RLock rLock = redissonClient.getLock("customer_lock");
rLock.lock();
// 省略代码
rLock.unlock();
}

其实,效果是一样的,无论使用 synchronized 还是分布式锁,最终导致锁失效的本质原因是解锁在提交事务之前

3. 解决方案

第一种方案就是在 Controller 层加锁了,类似于下面这样。

1
2
3
synchronized (this) {
return customerService.addCustomer(req);
}

第二种方案是在 Service 层里面再写一个方法,类似于下面这样。

1
2
3
4
5
6
7
@Override
public synchronized Result<Object> addCustomer(InsertCustomerReq req) {
return addCustomerOne(req);
}

@Transactional(rollbackFor = Exception.class)
public Result<Object> addCustomerOne(InsertCustomerReq req)

总结:

  • 在实际开发过程中,要确认好是否需要使用 @Transactional 进行事务管理,不要瞎用。
  • 在需要加锁并且需要保证事务的场景中,尤其需要注意锁是否在事务提交之前释放。

本文标题:Synchronized失效

原始链接:https://zhaoxiaofa.com/2019/02/21/Synchronized失效/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。