编程式事务和注解式事务的区别

背景

@Resource
private TransactionTemplate transactionTemplate;

@Override
public long addSpace(SpaceAddRequest spaceAddRequest, User loginUser) {
    // 在此处将实体类和 DTO 进行转换
    Space space = new Space();
    BeanUtils.copyProperties(spaceAddRequest, space);
    // 默认值
    if (StrUtil.isBlank(spaceAddRequest.getSpaceName())) {
        space.setSpaceName("默认空间");
    }
    if (spaceAddRequest.getSpaceLevel() == null) {
        space.setSpaceLevel(SpaceLevelEnum.COMMON.getValue());
    }
    // 填充数据
    this.fillSpaceBySpaceLevel(space);
    // 数据校验
    this.validSpace(space, true);
    Long userId = loginUser.getId();
    space.setUserId(userId);
    // 权限校验
    if (SpaceLevelEnum.COMMON.getValue() != spaceAddRequest.getSpaceLevel() && !userService.isAdmin(loginUser)) {
        throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限创建指定级别的空间");
    }
    // 针对用户进行加锁
    String lock = String.valueOf(userId).intern();
    synchronized (lock) {
        Long newSpaceId = transactionTemplate.execute(status -> {
            boolean exists = this.lambdaQuery().eq(Space::getUserId, userId).exists();
            ThrowUtils.throwIf(exists, ErrorCode.OPERATION_ERROR, "每个用户仅能有一个私有空间");
            // 写入数据库
            boolean result = this.save(space);
            ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
            // 返回新写入的数据 id
            return space.getId();
        });
        // 返回结果是包装类,可以做一些处理
        return Optional.ofNullable(newSpaceId).orElse(-1L);
    }
}

设计思路:使用本地 synchronized 锁对 userId 进行加锁,这样不同的用户可以拿到不同的锁,对性能的影响较低。在加锁的代码中,我们
使用 Spring 的 编程式事务管理器 transactionTemplate 封装跟数据库有关的查询和插入操作,而不是使用 @Transactional 注解来控制事务,这样可以保证事务的提交在加锁的范围内。

举例分析

一、首先通过JVM 级别的本地对象锁,锁的粒度是当前进程中该 userId 的字符串对象。如果两个不同的用户(比如 A 和 B),那么他们加锁的是两个完全不同的 userId,互不干扰,锁根本不会互斥

加锁只针对同一个用户生效,不会导致 A 阻塞 B,也不会阻止 B 的事务影响 A 的事务。使得十个用户也可以同时创建自己的私有空间

二、为什么使用编程式事务呢?

注解式事务会将方法交给Spring去管理,注解会等到锁释后,再帮用户 A 提交事务。

用户 A 连点 2 次提交“创建空间”,这两个请求几乎同时进入后端。

因为 userId 相同,synchronized(userId.intern()) 生效:第一个线程拿到锁,第二个阻塞等待。

第一个线程开始执行,进入 @Transactional 注解控制的事务中,查数据库没有空间,就插入了空间,但此时事务还没提交!

锁释放后,第二个线程继续执行,此时它用的是注解事务,数据库还看不到刚插入的空间(因为第一个事务没提交),所以它也查出“用户没有空间”,也插入。

最后两个事务提交,两个空间都写入了。

⚠️ 本质问题:
@Transactional 的事务是 Spring 托管的,事务提交时机在方法执行结束之后。而锁(synchronized)控制的是代码段的执行顺序,锁和事务的生命周期是错位的。

也就是说:
●锁释放 ≠ 事务提交
●锁释放时,数据库中的数据还可能是旧的

所以即使用户A对 userId 加了锁,只要事务没有提交完,其他线程查到的数据库数据依旧不准确,导致并发问题。

为了使得事务完全在锁的掌控内,即事务提交了,数据库也有新数据更新,才释放锁。

✅ 编程式事务的好处:

transactionTemplate.execute(status -> { … }) 是编程式事务,它的控制范围是你写的代码块,事务提交一定在锁控制范围之内
这样就避免了“事务没提交,锁先释放”的问题,保证了:
●每次进入临界区时,数据库一定是最新状态;
●同一个用户不会并发创建多个空间。

你可能感兴趣的:(后端技术总结,spring,boot)