行业资讯 2025年08月6日
0 收藏 0 点赞 202 浏览 2348 个字
摘要 :

文章目录 问题分析 解决方案 并发问题的解决 方法一:定时任务清理 方法二:使用 Lua 脚本 Lua 脚本实现 脚本加载与执行 加载 Lua 脚本 执行 Lua 脚本 在生产环境中……




  • 问题分析
  • 解决方案
  • 并发问题的解决
    • 方法一:定时任务清理
    • 方法二:使用 Lua 脚本
    • Lua 脚本实现
    • 脚本加载与执行
      • 加载 Lua 脚本
      • 执行 Lua 脚本

      在生产环境中,为了保存用户最近操作的 n 条数据,最初采用 Redis 的 key-value 结构,但因并发修改导致数据丢失。为解决此问题,改用 Redis 的 SortedSet,以时间戳作为 score,数据作为 value,实现类似 LRU 的功能,时间复杂度为 O(log(n))。为避免并发问题,采用 Lua 脚本保证操作的原子性。脚本通过 ZADD 添加数据,ZCARD 查询集合大小,并在超出限制时通过 ZREMRANGEBYRANK 删除多余数据。通过这种方式,成功解决了并发场景下的数据丢失问题,同时保证了业务功能的高效实现。

      在最近的生产环境中,我们遇到了一个业务场景:需要保存用户最近操作的 n 条数据。最初,我们采用了 Redis 的 key-value 结构,其中 value 是通过 JSON 序列化的数据集合。但在测试过程中,发现偶尔会出现数据丢失的问题。经过排查,问题根源在于并发修改导致的数据覆盖。

      问题分析

      在保存用户最近操作数据的场景中,很容易想到使用 LRU(Least Recently Used) 算法。然而,Redis 中并没有直接实现 LRU 的节点 + Map(例如 Java 中的 LinkedHashMap)。如果使用 Java 中的数据结构,就会像之前的 key-value 方案一样,面临并发问题。

      解决方案

      综合业务需求和并发特性,我们决定使用 Redis 的 SortedSet(有序集合)。通过将操作数据的时间戳作为 score,需要保存的数据作为 value,可以实现类似 LRU 的功能,且时间复杂度为 O(log(n))。虽然稍逊于 LinkedHashMapO(1),但由于每个 key 的数据量不会太大,性能完全能够满足要求。

      并发问题的解决

      在满足业务功能的基础上,还需要解决并发问题。如果使用原生命令,执行添加操作后,需要查询当前 set 的大小。如果大小超过设定的目标值,则删除多余的元素。但在并发场景下,这种方法很容易出现问题。例如,设定最大值为 5,当前 set 中已有 4 个元素,此时并发三个请求:

      1. 第一个请求保存数据后,在查询 set 大小之前,第二个请求也保存了数据。
      2. 此时,第一个请求会判断当前 set 超过目标值并进行删除。
      3. 在第一个请求删除之前,第二个请求也查询了 set 的大小,并进行删除。
      4. 同理,第三个请求也可能进行删除。

      这样会导致数据被重复删除,且并发请求越多,额外删除的数据也越多。
      针对这个问题,我们有两种解决方案:

      方法一:定时任务清理

      不在添加数据时进行删除操作,而是维护一个 key 的集合,通过定时任务定期检查数据并删除多余的部分。这种方法简单易实现,但可能会导致数据在短时间内超出目标值。

      方法二:使用 Lua 脚本

      Lua 脚本可以保证操作的原子性,从而有效解决并发问题。我们最终采用了这种方式。

      Lua 脚本实现

      以下是 Lua 脚本的详细代码:

      private static final String ADD_RECORD_SCRIPT =
              \"local key = KEYS[1] \" +
                      \"local member = ARGV[1] \" +
                      \"local score = ARGV[2] \" +
                      \"local maxSize = tonumber(ARGV[3]) \" +
                      \"redis.call(\'ZADD\', key, score, member) \" +
                      \"local size = redis.call(\'ZCARD\', key) \" +
                      \"if size > maxSize then \" +
                      \"    redis.call(\'ZREMRANGEBYRANK\', key, 0, size - maxSize - 1) \" +
                      \"end \" +
                      \"return 1\";

      脚本加载与执行

      加载 Lua 脚本

      private void initScript() {
          try {
              saveScriptSha = cluster.scriptLoad(ADD_RECORD_SCRIPT);
              log.info(\"加载lua脚本成功: {}\", saveInterviewScriptSha);
          } catch (Exception e) {
              log.error(\"加载lua脚本失败\", e);
              throw new IllegalStateException(\"加载lua脚本失败\", e);
          }
      }

      执行 Lua 脚本

      public void saveSingleRecord(String key, String value) {
          long score = System.currentTimeMillis() / 1000;
          int count = 0;
          while (++count <= MAX_TRY_COUNT) {
              try {
                  cluster.evalsha(saveScriptSha, Collections.singletonList(key), Arrays.asList(value, score + \"\", MAX_SIZE + \"\"), false);
                  return;
              } catch (JedisNoScriptException jedisNoScriptException) {
                  log.error(\"脚本不存在: {} {}\", key, value, jedisNoScriptException);
                  initScript();
              } catch (Exception e) {
                  log.error(\"保存失败: {} {}\", key, value, e);
                  throw new RuntimeException(e);
              }
          }
          throw new RuntimeException(\"保存失败\");
      }
      
      

      在执行过程中,需要注意脚本可能因为服务重启或执行 flush 命令等原因被清除。因此,当出现脚本不存在的异常时,我们会进行重试。

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/10255.html

管理员

相关推荐
2025-08-06

文章目录 一、Reader 接口概述 1.1 什么是 Reader 接口? 1.2 Reader 与 InputStream 的区别 1.3 …

988
2025-08-06

文章目录 一、事件溯源 (一)核心概念 (二)Kafka与Golang的优势 (三)完整代码实现 二、命令…

465
2025-08-06

文章目录 一、证明GC期间执行native函数的线程仍在运行 二、native线程操作Java对象的影响及处理方…

348
2025-08-06

文章目录 一、事务基础概念 二、MyBatis事务管理机制 (一)JDBC原生事务管理(JdbcTransaction)…

456
2025-08-06

文章目录 一、SnowFlake算法核心原理 二、SnowFlake算法工作流程详解 三、SnowFlake算法的Java代码…

517
2025-08-06

文章目录 一、本地Jar包的加载操作 二、本地Class的加载方法 三、远程Jar包的加载方式 你知道Groo…

832
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号