前言
今天现网有个订单卡单了,经过排查发现没有任何异常日志,根据日志定位发现本应该更新的一个状态,也sql肯定执行了(使用了Hibernate的ORM框架),但是数据库里面的状态没有更新。大概逻辑如下
String hql= from orderInfo where id="PTTest";
OrderInfo orderInfo=commomDao.findSingle(hql);
orderInfo.setStatus("completed");
commonDao.update(orderInfo);
根据日志能确认,此段代码肯定是被执行了,但是数据库表的Status依旧是processing。
问题排查
这种情况,怀疑是在Status更新成completed之后,其他的事务把Status更新了成processing了。想佐证这种猜测,直接看数据库的bin log日志是最直观的。找大数据组同事找了日志时间点周围的bin log日志
确实在更新completed之后,又被其他事务更新成processing。后来从ELK中查看到对应requestLog日志,发现确实有请求更新了Status。
问题解决
直接用update 语句更新需要更新的字段
update orderInfo set Status="completed" where id="PTTest";
这种写法,更偏向Mybatis了,违背了Hibernate的思想了。如果我们不想写sql,还是希望以对象形式更新数据。我们也可以写一个方法
public void update(Class<T> clazz, K id, Map<String, Object> columns) {
if (CollectionUtils.isEmpty(columns.keySet())) {
throw new ParameterException(RetCode.HIBERNATE_COLUMNS_ARGS_LENGTH_ERROR);
}
StringBuilder hqlBuilder = new StringBuilder(" update " + clazz.getSimpleName() + " set ");
columns.keySet().forEach(k -> hqlBuilder.append(k).append(" = :").append(k).append(", "));
hqlBuilder.append(" updateTime = :updateTime ");
hqlBuilder.append(" where id = :id ");
commonDao.execute((ExecuteCallback<Object>) session -> {
Query query = session.createQuery(hqlBuilder.toString());
columns.keySet().forEach(k -> query.setParameter(k, columns.get(k)));
query.setParameter("updateTime", new Date());
query.setParameter("id", id);
return query.executeUpdate();
});
}
那么我们在调用的时候
Map<String, Object> params = new HashMap<>();
params.put("status", "completed");
update(OrderInfo.class, orderInfo.getId(), params);
其实update方法内部还是在拼凑sql。
使用@version注解
使用@Version
注解是Hibernate提供的一种乐观锁机制,用于解决并发更新时的数据覆盖问题。乐观锁是一种锁策略,它假设多个事务在同一时间对同一条记录进行更新的可能性较低,因此不采用在更新前加锁的方式(即悲观锁),而是在更新时检查版本号,如果发现版本冲突,则认为有其他事务已经更新了该记录,此时可以选择重新获取最新数据并再次尝试更新,或者抛出异常告知用户。当使用@Version
注解标注在实体类的某个属性上时,Hibernate会在每次读取该记录时自动将版本号加1,并在更新时检查当前版本号与数据库中的版本号是否一致。如果版本号不一致,说明有其他事务已经更新了该记录,此时Hibernate会抛出OptimisticLockException
异常,从而避免了数据覆盖的问题。
使用@Version
注解的步骤如下:
- 选择版本号字段:选择一个合适的字段作为版本号字段,通常是一个数值类型或时间戳类型的字段。
- 添加注解:在实体类中的版本号字段上添加
@Version
注解。 - 处理异常:在业务层捕获
OptimisticLockException
异常,根据实际需求进行处理,例如提示用户重新操作或重试更新。
通过使用@Version
注解,可以在不使用显式锁的情况下,依靠版本号机制来控制并发更新,从而避免数据覆盖的问题。但需要注意的是,乐观锁并不能保证数据的绝对一致性,因为在检查版本号和实际更新之间仍然存在短暂的时间窗口,极端情况下仍然可能发生并发更新的问题。因此,在使用乐观锁时需要仔细评估并发风险,并结合实际业务需求进行选择。