当前位置: 首页 > 知识库问答 >
问题:

springboot2 + docker 线上运行时 redis setnx 两个java进程都获取到了锁?

东门楚
2023-04-20

发生企业级灾难
代码执行描述:
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工具是

@Component
public 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 自动注入的类

@Resource
private 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));代替

共有4个答案

韩照
2023-04-20

JedisPoolUtil 代码没有贴全吗?
没有看到 setRedisTemplate()方法?
另外,这行代码看起来比较奇怪:

JedisPoolUtil redisUtil = jedisPoolUtil.setRedisTemplate(...);

JedisPoolUtil.set方法又返回一个 JedisPoolUtil?

在不同的java进程里,jedisPoolUtilConfig.getRedisTemplateByDb(10) 连接到的是同一个redis服务吗?

蒯安平
2023-04-20

image.png 你们是一个库一个连接?能确保两个应用抢占同一个key的时候,是同一个库连接吗?

萧阳波
2023-04-20

涉及到核心交易类业务,还是使用mysql的锁机制来实现比较稳妥,redis在某些极端情况下可能会导致锁丢失(比如返回客户端成功后,但还没有将日志写入磁盘,此时redis服务挂掉将导致锁丢失)。

非核心业务为了方便可以使用redis的分布式锁,建议使用redisson,它里面已经实现了锁重入、锁续期这些机制,不建议用自己写的。

拓拔骁
2023-04-20

你这种情况可能存在多个进程连接到不同的 Redis 库,你用了 Spring Boot 的 RedisTemplate 来实现分布式锁,我建议你用 Redisson 分布式锁来解决这个问题,或者把进程都连接到相同的 Redis 库

 类似资料:
  • 问题内容: 每次我声明并运行两个服务时,我都遇到以下binder.proxy异常。一个服务在不同的进程中运行(专用于应用程序),另一项服务在与我的应用程序在同一应用程序(默认应用程序进程)中使用Binder实现运行的进程中运行。 AndroidManifest.xml: 我在MainActivity上的第一个按钮上单击启动我的第一个服务: MainActivity.java 与MainApplic

  • 本文向大家介绍Java实现的两个线程同时运行案例,包括了Java实现的两个线程同时运行案例的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了Java实现的两个线程同时运行。分享给大家供大家参考,具体如下: 运行结果: 更多java相关内容感兴趣的读者可查看本站专题:《Java进程与线程操作技巧总结》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作

  • 本文向大家介绍Shell脚本获取进程的运行时间,包括了Shell脚本获取进程的运行时间的使用技巧和注意事项,需要的朋友参考一下 在我们的系统中,我曾写了一个脚本去定时更新一些repository,但偶尔会遇到问题,比如:git pull之时可能会卡在那里(可能由于某时刻的网络问题),它会阻碍后面的下一次更新。 所以我就在想,我今后启动这个脚本时,进行检查,如果上次运行的脚本还没结束,而且过了某个时

  • 我试图制作一个时钟,如果 如何同时运行警告语音和时钟,使时钟在播放警告语音()时不会停止? 语音代码: 时钟代码:

  • 问题内容: 因此,我的目标是让函数启动自己的线程,以便可以并行运行,而不必等待上一个线程完成。问题在于,它似乎不是多线程的(意味着一个线程在另一个线程开始之前就完成了)。 我也有该函数的内部,但似乎启动的线程的标识与从其运行python脚本的主线程相同。我认为我的方法不正确。 问题答案: 这是常见错误,容易出错。 只需在主线程中一次执行您的函数,然后将(我想为您函数的返回值)作为函数传递给线程,这

  • 我在尝试停止运行多个线程的程序时遇到了问题,所有运行的线程都试图找到相同的解决方案,但一旦一个线程找到了解决方案,所有其他线程都将停止。 在main方法中,我创建了一个线程组,使用for循环向其中添加线程并启动它们 在实现Runnable的类中,我很难找出如何使其实现,以便一旦其中一个线程找到解决方案,所有线程都将停止。最终发生的情况是,要么其他线程继续运行,有时这些线程会相互中断并相互覆盖。