springboot2 + docker 线上运行时 redis setnx 两个java进程都获取到了锁?
发生企业级灾难
代码执行描述:
1、用户的余额是通过后台,在每日0点开始进行入账(归入余额)。
2、使用@Scheduled作为定时脚本
3、定时任务需要获取到 redis setnx 锁的权限,才能继续执行下面的代码
@Component // 1.主要用于标记配置类,兼备Component的效果。@EnableScheduling // 2.开启定时任务
public class OrderMoneyToMerchantMoney {
@Resource
private JedisPoolUtil jedisPoolUtil;
@Scheduled(cron = "${scheduled.timedTaskScheduled:*/5 * * * * ?}")
public void configureTasks() {
System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
selectDb();
}
@Transactional // 事务操作
public void selectDb() {
……
for (Orderlist temp : allList) {
// 获取锁权限
boolean setNx = JedisPoolUtil.setNx("D1MoneyToMerchantD0Money:" + temp.getOrderid(), 60 * 10); // 锁10分钟
if (!setNx) {
continue;
}
……
}
}
使用的util工具是
@Componentpublic class JedisPoolUtil {
private RedisTemplate<String, Object> redisTemplate;
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public boolean setNx(String key, long time) {
try {
Boolean lock = getRedisTemplate().opsForValue().setIfAbsent(key, 1, time, TimeUnit.SECONDS);
if (lock) {
// 加锁成功
return true;
} else {
// 没有分布式锁,等待,进入自旋
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
redis执行逻辑:
在程序初始化时:创建0~15库的redis连接,并放到map中
使用时,取出map中的一个值,加载到工具类的redisTemplate当中
JedisPoolUtil redisUtil = jedisPoolUtil.setRedisTemplate(jedisPoolUtilConfig.getRedisTemplateByDb(10));
服务器:一台 centos7.9
docker:docker19.03.13 + docker composer
java:运行在2个docker容器中(使用docker composer编排)
redis:运行在docker容器中 redis_6.2.6版本 使用docker官方镜像 redis:6.2.6
灾难描述:
1、用户余额在昨日,突然2个在docker中的java进程均获取到了 redis setnx 锁-->导致用户余额重复增加
2、商户余额已经提现,平台金额亏损。
发现原因:
1、猜测redis使用的spingboot2 自动注入的类
@Resourceprivate JedisPoolUtil jedisPoolUtil;
而不是
JedisPoolUtil redisUtil = jedisPoolUtil.setRedisTemplate(jedisPoolUtilConfig.getRedisTemplateByDb(10));
导致多个java进程(可能)连接到不同的redis库,所以不同redis库在setnx时,可以获取到锁。
2、猜测是否跟使用docker redis+docker java 有关
-->本地启动2个jar+本地redis测试结果:只能1个java进程获取到redis setnx锁,另一个获取不到
-->本地启动1个jar+线上启动2个docker java+本地连接线上redis测试结果:只能1个java进程获取到redis setnx锁,另一个获取不到
测试结果:均正常
3、查看docker redis状态
执行:docker ps | grep redis
结果:redis运行时长均在3week以上,没有提示docker redis中断重启
4、查看宿主机器上docker redis日志
在日志中查看到并无错误日志,日志为空
5、查看宿主机器上message内核日志
没有错误
解决办法:
1、猜不到是什么原因,将使用mysql for update互斥锁,进行锁表更新,如果未更新则回滚。
上线后,运行正常 测试正常。
2、怀疑redis连接库的原因 使用JedisPoolUtil redisUtil = jedisPoolUtil.setRedisTemplate(jedisPoolUtilConfig.getRedisTemplateByDb(10));代替
回答:
你这种情况可能存在多个进程连接到不同的 Redis 库,你用了 Spring Boot 的 RedisTemplate 来实现分布式锁,我建议你用 Redisson 分布式锁来解决这个问题,或者把进程都连接到相同的 Redis 库
回答:
涉及到核心交易类业务
,还是使用mysql的锁机制来实现比较稳妥,redis在某些极端情况下可能会导致锁丢失
(比如返回客户端成功后,但还没有将日志写入磁盘,此时redis服务挂掉将导致锁丢失)。
非核心业务为了方便可以使用redis的分布式锁,建议使用redisson,它里面已经实现了锁重入、锁续期这些机制,不建议用自己写的。
回答:
你们是一个库一个连接?能确保两个应用抢占同一个key的时候,是同一个库连接吗?
回答:
JedisPoolUtil 代码没有贴全吗?
没有看到 setRedisTemplate()方法?
另外,这行代码看起来比较奇怪:
JedisPoolUtil redisUtil = jedisPoolUtil.setRedisTemplate(...);
JedisPoolUtil.set方法又返回一个 JedisPoolUtil?
在不同的java进程里,jedisPoolUtilConfig.getRedisTemplateByDb(10) 连接到的是同一个redis服务吗?
回答:
只能是两个java实例,连接到不同的redis库了
redis执行逻辑:
在程序初始化时:创建0~15库的redis连接,并放到map中
使用时,取出map中的一个值,加载到工具类的redisTemplate当中
检查下这个jedisPoolUtilConfig和这个map的实现逻辑
比如初始化异常或者获取异常的时候,MAP里面15个redisTemplate是不是有使用库0一类的容错逻辑。
另外线上可以查一下是不是库0是不是有相同的key,包括你后边的本地链接测试的时候
建议
- 分组别分库
- 多实现的,使用别名来指定唯一性
- 关键业务增加必要的日志、通知告警一类的
以上是 springboot2 + docker 线上运行时 redis setnx 两个java进程都获取到了锁? 的全部内容, 来源链接: utcz.com/p/945121.html