一、场景:
多条mq消息同时被集群接收处理,需要修改的数据有设计行的乐观锁,由于业务问题,修改数据库同一行数据的时间总是会刚好撞上。
二、条件限制:
乐观锁本来是适用于读大于写的情况,这里写的并发很高,本来是不适用的,但是因为框架问题,不能改这个乐观锁的设计,所以我想出了如下的方案。
三、方案a:重试+随机睡眠
1. 失败重试
乐观锁检测版本号,多个并发修改只会成功一个,这里每条数据都不可丢弃,所以加上失败重试。此外,由于是同步另一套数据,为了提高响应,所以这里也做了异步处理@Async。
springboot重试的注解使用可以参考:
https://blog.csdn.net/weixin_34138139/article/details/91876530
@Async
@Retryable(value= {ActivitiOptimisticLockingException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000L, multiplier = 3))
public void func(...) {
// do ...
}
@Recover
public void recover(ActivitiOptimisticLockingException e) {
e.printStackTrace();
log.info("-");
}
需要注意的是重试方法和重试失败的回调方法返回值需要一致,异步方法的返回值也很特殊,这里不做介绍。
还有@Async、@Retryable的注解是靠代理类做增强的,如果非要本类调用需要创建或注入实例,或者干脆写到另一个类中去。
2. 随机数睡眠避开扎堆
类的属性增加:
private Random random = new Random();
生成随机数的方法:
我需要睡眠时间在0-3秒,所以这里private static final Integer SLEEP_MILLIS = 3000;
private Long getRandom() {
long timeMillis = System.currentTimeMillis();
int i = random.nextInt();
Long l = timeMillis / i % SLEEP_MILLIS + 1;
if(l < 0) {
l = getRandom();
}
return l;
}
在重试的方法里加上:
Long random = getRandom();
Thread.sleep(random);
四、改进,方案b:redis分布式锁 + 重试
需要注意的是,上面的设计有限制,mq消息先后顺序影响结果的就不适用。
有先后顺序的mq消息,被随机数睡眠调整了顺序,可能就会导致错误的结果。
虽然项目中消息的先后顺序并不影响结果,但是处理的时间变长了,最后采用redis分布式锁 + 重试的方案。