编程式事务是通过代码显式管理事务的一种方式,开发者直接在代码中手动控制事务的开启、提交或回滚。与之相对的声明式事务(如使用 @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);
});
关键特点
-
- 通过 TransactionTemplate 的 execute() 方法明确界定事务范围。
- 事务的提交或回滚由框架根据回调函数执行结果自动处理(无异常则提交,有异常则回滚)。
-
- 可在代码中根据条件动态决定是否开启事务。
- 适合需要精细控制事务边界的场景,例如在循环中分批处理数据并提交事务。
-
- 事务管理代码与业务逻辑耦合,增加了代码量。
- 声明式事务通过注解解耦,但灵活性较低。
-
- 可在 TransactionTemplate 中预设隔离级别、传播行为、超时时间等属性,例如:
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
代码中的事务逻辑
- 原子性保证:更新博客点赞数(blogService.lambdaUpdate())和插入点赞记录(this.save(thumb))被包裹在同一事务中,确保两者同时成功或失败。
- 异常回滚:若操作中抛出未捕获的异常(如 throw new RuntimeException("用户已经点赞")),事务会自动回滚。
- 锁与事务的协作:外层 synchronized 锁防止用户并发请求,而事务确保数据库操作的原子性,两者共同保障数据一致性。
适用场景
- 需要根据运行时条件动态决定是否开启事务。
- 涉及多个数据源或复杂事务链(需精细控制提交/回滚点)。
- 与旧代码集成时,声明式事务无法满足需求。
对比声明式事务
|
特性 |
编程式事务 |
声明式事务 |
|
控制方式 |
手动编写代码管理事务 |
通过注解或XML配置自动管理 |
|
灵活性 |
高(可精确控制事务边界) |
低(基于方法或类级别) |
|
代码侵入性 |
高(事务代码与业务逻辑混合) |
低(通过AOP代理解耦) |
|
可读性 |
较低(事务代码分散在业务逻辑中) |
较高(事务定义集中) |
总结
编程式事务通过代码直接控制事务,适用于需要高度定制化事务管理的场景,而示例代码通过 TransactionTemplate 确保了点赞操作与计数更新的原子性,结合同步锁有效解决了并发问题。
在项目中的体现
更新博客点赞数量和插入点赞记录都成功的时候才会提交事务,
同时防止用户的并发请求,也就是在短时间内,对一个博客进行多次点赞
注意

在这使用了本地锁,但是在分布式的场景下有可能会失效,如果在事务内加锁,也会导致本地锁失效
在事务内加锁有可能失效的原理
关键结论:
必须让锁的作用域完全包裹事务的操作,保证其他线程获取锁的收,前序事务必然已经完成了事务的提交。也就是:
先获取锁——》k 开启事务-》执行业务逻辑-》提交事务-》释放锁,保证点赞错做的原子性和一致性,不被其他线程干扰。
点赞和取消点赞功能优化
使用 redis 来减轻数据库的压力,但是 redis 中不存在数据,可能是因为缓存过期,或者是因为本来就没点赞,就必须要去数据库中查询,这样就会造成,先去 redis 查一次,再去 MySQL 中查询一次,不仅没有降低 mysql 的压力,反而多了一次查 reids 的过程
这样可以采用冷热分离的策略,比如,我们认为最近一个月锁发布的博客是热数据,那么可以让 redis 中点赞记录的存在时间是帖子的发布时间再多加上一个月。如果点赞的时候这个博客的发布的时间还不超过一个月的时间,就查询 redis 校验是否已经点赞,如果发布时的时间超过了一个月,就通过 mysql 及逆行校验是否已经点赞。还可以引入布隆过滤器,在过滤器中进行调整。
但是 redis 中是不知针对 hash节后的某个具体的属性进行设置过期时间的,但是可以调整 value 的实际结构,比如调整为:
{
"thumbId":xxx,
"expireTime":xxx
}
然后在内存中判断是否过期。



