使用Spring Data时如何避免Read-Modify-Write反模式?
先聊聊Craig Ringer提到的那个读-改-写循环反模式:很多人写代码时习惯先从数据库读数据,在应用层改完再写回去——比如查用户余额、判断后扣钱再更新。看起来没问题,但高并发下就会踩坑:两个会话同时操作同一条数据时,很容易覆盖彼此的修改。别以为事务能搞定一切,事务不是银弹,除非你锁表,但那性能代价太大了。
在Spring Data里,我们一不小心就会掉进这个坑,就像你给出的示例:先findByLastName查出Customer,改了firstName再save,这就是标准的读-改-写。你已经想到用自定义Repository方法直接执行更新SQL,但每个属性都写个方法确实太麻烦。下面给你几个更省心的方案:
1. 用JPQL写组合更新,避免单属性方法泛滥
不用每个属性单独写方法,而是根据需求写组合式的JPQL更新,比原生SQL更贴合JPA:
public interface CustomerRepository extends CrudRepository<Customer, Long> { Customer findByLastName(String lastName); @Modifying @Query("UPDATE Customer c SET c.firstName = :firstName WHERE c.id = :id") void updateFirstNameById(@Param("id") Long id, @Param("firstName") String firstName); // 多字段更新也能轻松扩展 @Modifying @Query("UPDATE Customer c SET c.firstName = :firstName, c.lastName = :lastName WHERE c.id = :id") void updateFullNameById(@Param("id") Long id, @Param("firstName") String firstName, @Param("lastName") String lastName); }
这样你可以根据业务场景写对应的更新方法,不用每个字段都单独写,兼顾了简洁性和性能。
2. 加乐观锁,给读-改-写模式上保险
如果业务场景必须先读再改(比如需要基于原有字段值做判断),那可以用乐观锁来避免并发覆盖问题。给实体加个版本字段就行:
@Entity public class Customer { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private String firstName; private String lastName; @Version // 乐观锁版本标识 private Integer version; // 构造方法、toString、getter/setter都保留 }
当你调用save时,JPA会自动对比版本号:如果数据库里的版本和你手里的实体版本不一致,就会抛出OptimisticLockingFailureException,这时你可以捕获异常重试,保证数据不会被悄咪咪覆盖。不过这只是缓解问题,并没有完全消除读-改-写,性能上还是不如直接SQL更新。
3. 写个通用更新工具类,一招解决所有单字段更新
如果你的项目里有大量实体需要更新单个字段,写一堆方法太繁琐,不如整个通用工具类,用JPA的Criteria API动态生成更新语句:
@Component public class JpaDynamicUpdateHelper { @PersistenceContext private EntityManager entityManager; public <T> void updateSingleField(Class<T> entityClass, Long id, String fieldName, Object fieldValue) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaUpdate<T> updateQuery = cb.createCriteriaUpdate(entityClass); Root<T> root = updateQuery.from(entityClass); // 设置要更新的字段和值 updateQuery.set(root.get(fieldName), fieldValue); // 条件:匹配ID updateQuery.where(cb.equal(root.get("id"), id)); entityManager.createQuery(updateQuery).executeUpdate(); } }
然后在业务代码里直接调用就行,不管哪个实体、哪个字段都能用:
// 更新Customer的firstName jpaDynamicUpdateHelper.updateSingleField(Customer.class, customer.getId(), "firstName", "kek");
这种方式灵活性拉满,不用再写一堆重复的Repository方法。
4. 试试Spring Data JDBC(如果不需要JPA的全量特性)
如果你的业务不需要JPA的复杂ORM功能(比如关联映射、懒加载),Spring Data JDBC是个不错的选择——它更偏向SQL优先,能更直接地写更新语句,从根源上避免读-改-写循环,代码也更简洁。
总结一下
- 最优解:直接用SQL/JPQL完成更新,彻底跳过读-改-写,性能和并发安全性都是最好的;
- 妥协方案:如果必须读-改-写,加乐观锁避免数据覆盖;
- 偷懒方案:通用工具类解决大量单字段更新的繁琐问题。
内容的提问来源于stack exchange,提问作者Pt. Terk

