Spring中什么是编程式事务,和声明式事务有什么区别(点赞系统实战)

67 阅读4分钟

编程式事务是通过代码显式管理事务的一种方式,开发者直接在代码中手动控制事务的开启、提交或回滚。与之相对的声明式事务(如使用 @Transactional 注解)则是通过配置自动管理事务边界。

在示例代码中的体现

transactionTemplate.execute(status -> { ... }) 是典型的编程式事务实现:

return transactionTemplate.execute(status -> {
    // 事务内的数据库操作
    boolean update = blogService.lambdaUpdate()
            .eq(Blog::getId, blogId)
            .setSql("thumbCount = thumbCount + 1")
            .update();

    Thumb thumb = new Thumb();
    thumb.setUserId(loginUser.getId());
    thumb.setBlogId(blogId);
    return update && this.save(thumb);
});

  1. 显式控制
    • 通过 TransactionTemplate 的 execute() 方法明确界定事务范围。
    • 事务的提交或回滚由框架根据回调函数执行结果自动处理(无异常则提交,有异常则回滚)。
  1. 灵活性
    • 可在代码中根据条件动态决定是否开启事务。
    • 适合需要精细控制事务边界的场景,例如在循环中分批处理数据并提交事务。
  1. 代码侵入性
    • 事务管理代码与业务逻辑耦合,增加了代码量。
    • 声明式事务通过注解解耦,但灵活性较低。
  1. 事务配置
    • 可在 TransactionTemplate 中预设隔离级别、传播行为、超时时间等属性,例如:
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);

代码中的事务逻辑

  • 原子性保证:更新博客点赞数(blogService.lambdaUpdate())和插入点赞记录(this.save(thumb))被包裹在同一事务中,确保两者同时成功或失败。
  • 异常回滚:若操作中抛出未捕获的异常(如 throw new RuntimeException("用户已经点赞")),事务会自动回滚。
  • 锁与事务的协作:外层 synchronized 锁防止用户并发请求,而事务确保数据库操作的原子性,两者共同保障数据一致性。

适用场景

  • 需要根据运行时条件动态决定是否开启事务。
  • 涉及多个数据源或复杂事务链(需精细控制提交/回滚点)。
  • 与旧代码集成时,声明式事务无法满足需求。

对比声明式事务

特性编程式事务声明式事务
控制方式手动编写代码管理事务通过注解或XML配置自动管理
灵活性高(可精确控制事务边界)低(基于方法或类级别)
代码侵入性高(事务代码与业务逻辑混合)低(通过AOP代理解耦)
可读性较低(事务代码分散在业务逻辑中)较高(事务定义集中)

总结

编程式事务通过代码直接控制事务,适用于需要高度定制化事务管理的场景,而示例代码通过 TransactionTemplate 确保了点赞操作与计数更新的原子性,结合同步锁有效解决了并发问题。

在项目中的体现

更新博客点赞数量和插入点赞记录都成功的时候才会提交事务,

同时防止用户的并发请求,也就是在短时间内,对一个博客进行多次点赞

image.png

在这使用了本地锁,但是在分布式的场景下有可能会失效,如果在事务内加锁,也会导致本地锁失效

在事务内加锁有可能失效的原理

  1. 线程 A 获取锁-》执行数据库操作-》释放锁->事务提交
  2. 线程 B 在锁被释放之后立即获取锁
  3. 在默认的隔离级别可重复读下,由于在 A 提及爱哦之前 B 已经开启了事务,所以 B 此时只能督导 A 操作之前的数据,导致重复操作

关键结论:

必须让锁的作用域完全包裹事务的操作,保证其他线程获取锁的收,前序事务必然已经完成了事务的提交。也就是:

先获取锁——》k 开启事务-》执行业务逻辑-》提交事务-》释放锁,保证点赞错做的原子性和一致性,不被其他线程干扰。

点赞和取消点赞功能优化

使用 redis 来减轻数据库的压力,但是 redis 中不存在数据,可能是因为缓存过期,或者是因为本来就没点赞,就必须要去数据库中查询,这样就会造成,先去 redis 查一次,再去 MySQL 中查询一次,不仅没有降低 mysql 的压力,反而多了一次查 reids 的过程

这样可以采用冷热分离的策略,比如,我们认为最近一个月锁发布的博客是热数据,那么可以让 redis 中点赞记录的存在时间是帖子的发布时间再多加上一个月。如果点赞的时候这个博客的发布的时间还不超过一个月的时间,就查询 redis 校验是否已经点赞,如果发布时的时间超过了一个月,就通过 mysql 及逆行校验是否已经点赞。还可以引入布隆过滤器,在过滤器中进行调整。

但是 redis 中是不知针对 hash节后的某个具体的属性进行设置过期时间的,但是可以调整 value 的实际结构,比如调整为:

{
    "thumbId":xxx,
    "expireTime":xxx
}

然后在内存中判断是否过期。