Spring事务与锁的一些事

1.Spring事务与synchronized顺序问题

结论:

保证释放锁在事务提交之后

当一个方法加上事务后,在执行前要先开启事务,然后再执行目标方法,当目标方法执行完后提交事务。
自然获取锁是在开启事务后才执行的操作,一个线程获取到锁,到执行完业务再到释放锁后,此时事务还未提交,此时另外一个线程获取到锁,读到的数据还是旧值,就出现了线程安全问题

错误使用案例1

@Override
@Transactional(rollbackFor = Exception.class)
public void userIsExist(String userId) {

    synchronized (this) {													//1
        SysUser sysUser = this.userInfoMapper.selectUserInfo(userId);
        if (sysUser == null) {
            SysUser saveSysUser = new SysUser();
            saveSysUser.setUserName("zz");
            saveSysUser.setNickName("zz");
            saveSysUser.setUserId(Long.valueOf(userId));
            this.userInfoMapper.insert(saveSysUser);
        } else {
            this.userInfoMapper.updateAgeById(userId);
        }
    }

}

错误使用案例2

@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void userIsExist(String userId) {

    SysUser sysUser = this.userInfoMapper.selectUserInfo(userId);
    if (sysUser == null) {
        SysUser saveSysUser = new SysUser();
        saveSysUser.setUserName("zz");
        saveSysUser.setNickName("zz");
        saveSysUser.setUserId(Long.valueOf(userId));
        this.userInfoMapper.insert(saveSysUser);
    } else {
        this.userInfoMapper.updateAgeById(userId);
    }

}

分析:

当并发线程都执行到1时,其中有一个线程A率先抢到了锁,然后执行业务,保存了一条记录,释放锁

此时其他线程可以去抢锁,其中有另外一个线程B也抢到了锁,然后执行业务,保存了一条记录(报错,主键不能重复)

正确使用案例

@PostMapping("isExist")
public void userIsExist(@RequestParam("userId") String userId) throws InterruptedException {
    synchronized (this) {
        this.userInfoService.userIsExist(userId);
    }

}

多个线程先锁住资源,在线程内把事务提交后再释放锁。

Spring事务源码分析

事务实现原理

当代理对象调用目标Bean方法时,最终会执行TransactionAspectSupport.invokeWithinTransaction增强方法。

image-20230805212749479

createTransactionIfNecessary方法最终会调用DataSourceTransactionManager中的doBegin方法,该方法会从数据源中拿到一个数据库连接,将以数据源作为key,数据库连接作为value放入一个HashMap中,并将这个map放入ThreadLocal中,目的是让在同一个线程内执行不同的方法自始至终使用的是同一个数据库连接对象,从而保证同一个线程内自始至终使用的是同一个事务。其中真正开始事务的是setAutoCommit=false,如下图:

image-20230805213547077

问题与解答

问题1:事务真正提交的代码?

commitTransactionAfterReturning()方法,最终调用Connection.commit()方法

image-20230806231950579

问题2:当一个事务方法与到另外一个可执行的事务方法时,想使用传播机制时,是怎么调用的呢?

当调用createTransactionIfNecessary()方法时,最终会调用AbstractPlatformTransactionManager.getTransaction()方法,其中会先执行doGetTransaction()方法,该方法是DataSourceTransactionManager中的,会从当先线程中拿到数据库连接并使用
image-20230805214924566

问题3:事务默认回滚针对哪些异常?

DefaultTransactionAttribute.rollbackOn()方法

image-20230805220452069

问题4:非public方法无法被事务管理源码出处?

image-20230808002628591

总结

  • Spring AOP方法增强使用的是动态代理。本质上使用的jdk动态代理或者cglib动态代理
  • Spring事务实现上下文数据库连接传递使用的是ThreadLocal,保证在一个线程内使用的数据库连接是同一个
  • Spring事务增强逻辑采用的是环绕通知方式
所以从上边源码可见,Spring事务使用的是环绕通知,在真正执行代码时是开启事务的(connection.setAutoCommit(false)),然后再执行目标方法

2.Spring事务与AOP实现分布式锁执行顺序问题

业务流程:
1. 开启事务
2. 加锁
3. 单据是否已审核?已审核则中断程序,未审核则继续往下走
4. 修改单据审核状态为已审核
5. 记录单据相关流水
6. 释放锁
7. 提交事务

先说结论:
默认情况下Spring事务的优先级先于自定义aop的优先级的,如果一个service既被事务管理又被aop实现的分布式锁管理,那么事务会先执行,锁释放操作在事务提交前,这时锁的互斥作用就会失效,也会出现并发安全问题

抽象理解:
并发两个请求过来,有A,B两个线程处理
1. A、B两个线程同时开启了事务
2. A线程获取锁,执行业务,释放锁,但还未提交事务。
3. B线程在A线程释放锁后立即获取到了锁,此时数据库中目前还是旧数据,导致B线程查到的数据时和A线程查到的数据是一样的,还是出现了并发问题

实际问题理解:
1. A、B两个线程同时开启了事务
2. A线程获取锁,单据未审核,修改审核状态为已审核,记录流水,释放锁,此时事务还未提交
3. B线程在A线程释放锁后立即获取到了锁,此时当前单据还是未审核,修改审核状态为已审核,再次记录流水,出现了并发问题

源码分析

事务执行流程

TransactionAspectSupport

image-20230804223806638

AOP实现分布式锁

@Slf4j
@Aspect
@Component
public class RepeatParamAspect {

@Autowired
private RedissonClient redissonClient;

@Pointcut("@annotation(com.abucloud.annotation.RepeatLimit)")
public void pointCut() {
}

@Around("pointCut()")
public Object handleRepeat(ProceedingJoinPoint joinPoint) throws Throwable {

    // 通过ip+类名+方法名+参数值,来作为锁标识
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();

    String key = IpUtils.getIpAddr()
            + ":"
            + signature.getDeclaringTypeName()
            + ":"
            + method.getName()
            + ":"
            + Arrays.asList(joinPoint.getArgs());
    String lockKey = DigestUtils.md5DigestAsHex(key.getBytes());
    RLock lock = this.redissonClient.getLock(lockKey);

    RepeatLimit repeatLimit = method.getAnnotation(RepeatLimit.class);
    int lockTime = repeatLimit.lockTime();
    boolean lockSuccess = lock.tryLock(0, lockTime, TimeUnit.SECONDS);
    if (!lockSuccess) {
        throw new RuntimeException(lockTime + "秒内请勿重复提交");
    }

    // 执行handler
    Object result;
    try {
        result = joinPoint.proceed();
    } finally {
        lock.unlock();
    }
    return result;
}

}

业务逻辑

@RepeatLimit
@Transactional(rollbackFor = Exception.class)
public void updateNoticeLock(Integer documentId) {

    // 查询
    DocumentBO documentBO = this.sysUserMapper.selectDocument(documentId);
    // 幂等校验
    if (documentBO.getStatus().equals("1")) {
        throw new RuntimeException("单据已审核");
    }
    this.sysUserMapper.approve(documentBO);

    documentBO.setCreateTime(LocalDateTime.now());
    this.sysUserMapper.insertDocumentFlows(documentBO);
}

实验一

未指定分布锁aop事务优先级,也就是默认情况下,先是释放锁,然后走提交事务

image-20230804224523466 image-20230804224610292

实验二

指定分布锁aop事务优先级为最高后,流程是先提交事务,然后释放锁。可以自行实验一下哈


3.Lock.lock()与try{ }顺序问题

结论:先获取锁再try

为什么不先try{}再尝试获取锁呢?

如果先执行try{}再尝试获取锁,如果获取不到锁,抛出异常,最后会走finally让锁被释放,会导致出现异常问题,都没有获取到锁就要释放锁???根本不合理
posted @ 2023-08-17 13:09  永无八哥  阅读(805)  评论(0编辑  收藏  举报