编程式事务是通过代码显式管理事务的一种方式,开发者直接在代码中手动控制事务的开启、提交或回滚。与之相对的声明式事务(如使用 @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 确保了点赞操作与计数更新的原子性,结合同步锁有效解决了并发问题。
在项目中的体现
更新博客点赞数量和插入点赞记录都成功的时候才会提交事务,
同时防止用户的并发请求,也就是在短时间内,对一个博客进行多次点赞
在这使用了本地锁,但是在分布式的场景下有可能会失效,如果在事务内加锁,也会导致本地锁失效
在事务内加锁有可能失效的原理
- 线程 A 获取锁-》执行数据库操作-》释放锁->事务提交
- 线程 B 在锁被释放之后立即获取锁
- 在默认的隔离级别可重复读下,由于在 A 提及爱哦之前 B 已经开启了事务,所以 B 此时只能督导 A 操作之前的数据,导致重复操作
关键结论:
必须让锁的作用域完全包裹事务的操作,保证其他线程获取锁的收,前序事务必然已经完成了事务的提交。也就是:
先获取锁——》k 开启事务-》执行业务逻辑-》提交事务-》释放锁,保证点赞错做的原子性和一致性,不被其他线程干扰。
点赞和取消点赞功能优化
使用 redis 来减轻数据库的压力,但是 redis 中不存在数据,可能是因为缓存过期,或者是因为本来就没点赞,就必须要去数据库中查询,这样就会造成,先去 redis 查一次,再去 MySQL 中查询一次,不仅没有降低 mysql 的压力,反而多了一次查 reids 的过程
这样可以采用冷热分离的策略,比如,我们认为最近一个月锁发布的博客是热数据,那么可以让 redis 中点赞记录的存在时间是帖子的发布时间再多加上一个月。如果点赞的时候这个博客的发布的时间还不超过一个月的时间,就查询 redis 校验是否已经点赞,如果发布时的时间超过了一个月,就通过 mysql 及逆行校验是否已经点赞。还可以引入布隆过滤器,在过滤器中进行调整。
但是 redis 中是不知针对 hash节后的某个具体的属性进行设置过期时间的,但是可以调整 value 的实际结构,比如调整为:
{
"thumbId":xxx,
"expireTime":xxx
}
然后在内存中判断是否过期。