工业互联网实时数据统计一致性保障 — 基于 Redis Lua 的并发安全方案

工业互联网实时数据统计一致性保障 --- 基于 Redis Lua 的并发安全方案

1. 场景描述

在工业互联网平台中,设备(如机床、机械手、检测机等)持续上报累计计数器数据:

json 复制代码
{"deviceId": "D001", "output": 933560, "timestamp": 1718000000000}

平台需要实时计算每个设备在每小时内的产量增量,并将结果持久化到数据库,供报表系统查询。

核心计算逻辑

复制代码
收到设备上报的 output(累计计数器)
  → 从缓存取上次记录的 output 值
  → delta = 当前 output - 上次 output  
  → 数据库累加 delta(本小时产量 += delta)
  → 把当前 output 写回缓存

看似简单,实则在并发场景下存在数据不一致风险。

2. 问题现场

以某台设备在 12:00-13:00 一小时的运行数据为例:

数据来源 产量 说明
设备原始上报记录 153 output 从 933560 → 933713,真实增量
平台计算结果 169 比真实值多了 16

差异 16 件,误差率 10.5%。对于月累计报表,这个误差会逐小时放大,最终严重偏离真实产能。

排查过程

对比设备原始报文和平台处理日志,发现同一秒内存在重复处理

复制代码
13:10:13.195 [线程A] output=422 last=418 delta=4
13:10:13.196 [线程B] output=422 last=418 delta=4   ← 同一数据被两个线程各自计算

在该小时 167 次增量计算中,有 15 次是重复的,恰好对应多出来的 16 件(含一次大跳变带来的 1 件差异)。

3. 根因分析

原实现代码:

java 复制代码
// 步骤1:从缓存读取上次的计数器值
Long lastOutput = cache.get(key);            

// 步骤2:计算增量
long delta = currentOutput - lastOutput;     

// 步骤3:更新数据库
database.update("产量 += " + delta);         

// 步骤4:把当前值写回缓存
cache.set(key, currentOutput);               

问题:这四步不是原子操作。 当两个线程同时处理同一条消息时:

复制代码
时间轴 →

线程A: ①读缓存→418    ②delta=4    ③数据库+4    ④缓存写422
线程B: ①读缓存→418    ②delta=4    ③数据库+4    ④缓存写422
        ↑ 都读到旧值 ↑               ↑ 加了两次 ↑

两个线程在线程 A 写回缓存之前 都读到了旧值 418,各自算出 delta=4,各自写入数据库。本该 +4 变成了 +8

这是一个典型的 Read-Modify-Write 竞态条件。

4. 解决方案:Redis Lua 原子脚本

4.1 设计思路

将「读取旧值 + 比较 + 写入新值」三步合并成一个不可分割的原子操作,利用 Redis 单线程执行 Lua 脚本的特性保证并发安全。

4.2 Lua 脚本实现

lua 复制代码
-- KEYS[1]: 缓存 key(如 "counter:D001_2024061012")
-- KEYS[2]: 当前计数器值(字符串形式,如 "422")
-- KEYS[3]: 过期时间(秒,如 "86400")

local nv = tonumber(KEYS[2])
local ttl = tonumber(KEYS[3])
local old = redis.call('GET', KEYS[1])

-- 情况1:key 不存在(本周期首条数据)
if old == false then
  redis.call('SET', KEYS[1], nv)
  if ttl > 0 then redis.call('EXPIRE', KEYS[1], ttl) end
  return nv
end

old = tonumber(old)

-- 情况2:旧值无法解析为数字(历史遗留的序列化数据)
if old == nil then
  redis.call('SET', KEYS[1], nv)
  if ttl > 0 then redis.call('EXPIRE', KEYS[1], ttl) end
  return nv
end

-- 情况3:新值 > 旧值 → 正常增长
if nv > old then
  redis.call('SET', KEYS[1], nv)
  if ttl > 0 then redis.call('EXPIRE', KEYS[1], ttl) end
  return old
end

-- 情况4:新值 <= 旧值 → 重复数据或回退,不更新
return old

4.3 Java 侧封装

java 复制代码
public Long getAndSetGreater(String key, long newVal, long ttlSeconds) {
    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
    script.setScriptText(lua);
    script.setResultType(Long.class);
    return redisTemplate.execute(script,
            Arrays.asList(key, String.valueOf(newVal), String.valueOf(ttlSeconds)));
}

调用方简化为一行:

java 复制代码
// 原子的 "读取上次值 + 更新为当前值"
long lastOutput = cache.getAndSetGreater(key, currentOutput, 24 * 3600);
long delta = currentOutput - lastOutput;
if (delta > 0) {
    database.update("产量 += " + delta);
}

4.4 为什么能保证原子性

Redis 执行 Lua 脚本期间,其他命令必须排队等待。两个线程同时提交:

复制代码
Redis 单线程执行队列:

  [线程A脚本] → GET:418 → SET:422 → 返回418
  [线程B脚本] → GET:422 → 不更新  → 返回422
  
结果:A 得 delta=4 ✓    B 得 delta=0 ✓

线程B 读到的是线程A 刚写入的 422,delta 为 0,不会重复累加。

5. 踩坑记录:序列化兼容问题

5.1 JDK 序列化导致 Lua 脚本解析失败

部署后日志报错:

复制代码
ERR Error running script: attempt to compare number with nil

原因RedisTemplate<String, Object> 默认使用 JdkSerializationRedisSerializer,数字 422 被序列化为 Java 二进制字节:

复制代码
\xac\xed\x00\x05sr\x00\x11java.lang.Long\x00\x00\x00\x00\x00\x00\x01\xa6...

Lua 的 tonumber() 无法解析这种二进制格式,返回 nil,导致后续比较报错。

5.2 解决方案

RedisTemplate 有两套序列化器:

  • Key 序列化器 :默认 StringRedisSerializer(纯字符串,Lua 可解析)
  • Value/Args 序列化器 :默认 JdkSerializationRedisSerializer(二进制,Lua 不可解析)

将业务参数从 ARGV(走 Value 序列化器)移至 KEYS(走 Key 序列化器):

java 复制代码
// ❌ ARGV 走 Value 序列化 → 二进制 → Lua tonumber() 返回 nil
execute(script, singletonList(key), String.valueOf(newVal))

// ✅ KEYS 走 Key 序列化 → 纯字符串 → Lua tonumber() 正常解析
execute(script, Arrays.asList(key, "422", "86400"))

Lua 脚本对应使用 KEYS[2]KEYS[3] 替代 ARGV[1]ARGV[2]

6. 方案总结

核心思想

用 Redis Lua 脚本将"读-改-写"三个独立操作合并为一个原子操作,借助 Redis 单线程执行模型消除竞态条件。

适用场景

任何需要基于「上次值」计算「增量」的场景:

行业 场景举例
工业互联网 设备产量/能耗累计计算
物联网 传感器累计值增量统计
电商 商品库存扣减
金融 账户余额变动
游戏 玩家积分/经验值变更

边界情况全覆盖

场景 脚本行为 业务侧处理
周期首发(key 不存在) 写入当前值,返回当前值 delta=0,不累加
正常递增 写入新值,返回旧值 delta > 0,正常累加
重复数据(并发/重试) 不更新,返回当前值 delta=0,跳过
计数器回退(硬件重置) 不更新,返回旧值 delta<0,丢弃
旧版序列化数据 覆盖为纯数字字符串 delta=0,自动修复

注意事项

  1. Lua 脚本不要有副作用之外的长耗时操作(如网络请求),会阻塞 Redis
  2. Key 的序列化器必须是 String 类型 ,否则 Lua 中 tonumber() 同样失败
  3. 过期时间兜底:即使代码异常未清理,Key 也会自动过期,不会无限积压

7. 其他可选方案对比

解决 Read-Modify-Write 竞态条件不止 Lua 一种方式,以下是五种替代方案及其权衡。

7.1 分布式锁

处理前先获取锁,拿到锁才执行"读-改-写",否则跳过。

复制代码
lock = getLock("device:D001:hour:12")
if lock.tryLock(1秒) {
    读缓存 → 算delta → 写DB → 写缓存
    lock.unlock()
}
维度 评价
优点 代码改动小,逻辑直观
缺点 每次加解锁有网络往返开销;需处理锁超时、死锁、锁粒度

7.2 Redis WATCH + MULTI/EXEC(乐观锁)

Redis 原生事务机制:WATCH 监控 key,执行 EXEC 时若 key 已被其他连接修改,事务自动回滚,应用层重试。

复制代码
WATCH key
old = GET key
MULTI
SET key newVal
EXEC  → 若被改过返回 nil,重试
维度 评价
优点 Redis 原生支持,无需脚本
缺点 需实现重试逻辑;并发越高重试越多,CPU 浪费严重

7.3 单线程串行消费

deviceId 取模,将同一设备的消息路由到固定线程,天然串行。

复制代码
executor[deviceId.hashCode() % 线程数].submit(task)
维度 评价
优点 极简,无需任何锁或脚本
缺点 仅适用于消息队列等可控消费端;线程数固定,设备多则排队延迟;不适用于 HTTP/网关等多入口架构

7.4 数据库原子 UPDATE

将缓存逻辑下沉到数据库,一条 SQL 完成比较+更新:

sql 复制代码
UPDATE counter 
SET last_val = :newVal, 
    total = total + (:newVal - last_val)
WHERE device_id = ? AND hour = ? AND last_val < :newVal

affected rows = 0 表示并发冲突或数据未变化,跳过即可。

维度 评价
优点 去掉对 Redis 的依赖,天然持久化
缺点 每次报文都击穿到 DB,高频写入下数据库压力大;行锁排队性能显著低于 Redis

7.5 消息去重

在入口处用 Redis SETNX 或布隆过滤器标记已处理的消息 ID,防止重复消费。

复制代码
if (!cache.setNX("msgId:" + messageId, "1", 60秒)) {
    return  // 已处理,跳过
}
维度 评价
优点 从源头消除重复,改动最小
缺点 依赖 messageId 全局唯一且不变;不能解决两条不同消息恰好同一 output 值的并发;对乱序到达无保护

7.6 综合对比

方案 复杂度 性能 可靠性 额外依赖 适用场景
Lua 原子脚本 最高 最可靠 通用,首选
分布式锁 较低 需处理死锁 锁组件 锁粒度可控的场景
WATCH/MULTI 可靠 并发不高的场景
单线程消费 可靠 特定架构 消息队列消费者
DB 原子 UPDATE 最低 可靠 低频写入场景
消息去重 有限 需 messageId 重复投递是唯一问题源时

7.7 为什么本场景选择 Lua

  • 报文频率高(单设备每秒 3~5 条上报)→ 分布式锁和 DB UPDATE 会成为瓶颈
  • 消息可能由不同线程并行处理(网关/协议适配层多线程解码)→ 单线程消费不适用
  • 计数器存在跳变和回退(传感器噪声)→ 纯比较大小逻辑需要在数据面完成,数据库 SQL 难以表达复杂边界
  • Redis 已在架构中(缓存设备最新状态)→ 不引入额外组件

Lua 方案一次 Redis 往返完成"读 + 比较 + 写",零额外网络开销,100% 原子,在本场景下是综合最优解。

我是一名工业互联网开发工程师,服务于纵横工业互联网团队,这是河南863旗下专注工业数字化的团队,也是深耕工业数字化转型领域的专业技术与解决方案服务商。我们聚焦工业企业智能化升级核心需求,打造出全栈式、可落地的工业互联网产品与服务体系,构建了自主可控的五大核心产品体系,包括面向产业集聚区/工业园区的产业集聚区工业互联网管理平台,以及面向工业企业全生产流程的物联网平台、能耗能碳管理平台、设备管理系统、MES生产制造执行系统。

相关推荐
一拳一个娘娘腔1 小时前
CVE-2026-46300 — “Fragnesia“ 深度拆解:当修复补丁亲手唤醒了另一只恶魔
linux·安全
中创云图1 小时前
GPRSEEK 大模型:地下安全的 “AI 医生“
人工智能·安全
swordbob1 小时前
prototype 注入到 singleton 里,prototype是否还是线程安全的
安全·spring·单例模式·原型模式
风曦Kisaki1 小时前
#Linux监控与安全Day01:Zabbix部署全流程,基础监控配置与自定义监控项
linux·运维·安全·云计算·zabbix
顾凌陵1 小时前
PHP序列化漏洞实战:反序列化攻击的奥秘
安全·网络安全
云边云科技_云网融合10 小时前
云边云科技亮相 2026 WOD 制造业数智化博览会 云网融合赋能制造焕新
人工智能·科技·安全·制造
轻刀快马11 小时前
Redis 架构进阶:全景解析 RDB、AOF 与混合持久化机制
redis
56AI12 小时前
2026 企业级AI智能体开发平台推荐:聚焦底层安全与准确率的智能体平台
人工智能·安全·智能体
站斧小威13 小时前
TikTok跨境电商浏览器怎么使用:多账号防关联,IP独立隔离
安全