如何在Redis中实时计算成交量加权平均价(VWAP)
Redis侧无应用层全量加载实现VWAP聚合计算方案
核心逻辑
放弃每次报价变动时拉取品种下所有行情源数据全量计算的思路,为每个交易品种维护两个聚合中间值,所有计算在Redis服务端完成,全程不需要回传全量报价到业务层:
- 加权总成交额(VWAP公式分子:Σ(单源价格 * 单源成交量))
- 累计总成交量(VWAP公式分母:Σ(单源成交量))
最终VWAP值直接通过加权总成交额 / 累计总成交量计算得出。单源报价变动时仅做增量差值更新,时间复杂度O(1),不受行情源数量增长影响。
具体实现
1. 存储结构设计
保留原有单源报价的Hash存储结构,额外新增3个固定Hash键存储聚合数据,不需要修改现有单源报价的写入格式:
- 原有单源报价键:命名规则保持
{交易品种}:{行情源ID},Hash字段存price(价格)、volume(成交量) - 聚合键
agg:vwap:numerator:Hash类型,field为交易品种ID(如XYZ、ABC),value为对应品种的加权总成交额 - 聚合键
agg:vwap:denominator:Hash类型,field为交易品种ID,value为对应品种的累计总成交量 - 结果键
agg:vwap:result:Hash类型,field为交易品种ID,value为对应品种最终计算的VWAP价格
2. 原子更新计算Lua脚本
所有行情源的报价更新操作,统一调用预加载的Lua脚本执行,全程原子完成增量计算,应用层仅需传入新报价参数即可直接拿到最新VWAP结果,不需要拉取任何其他行情源的数据。
脚本内容如下:
-- KEYS入参顺序:单源报价键、聚合分子键、聚合分母键、VWAP结果键 -- ARGV入参顺序:交易品种ID、最新价格、最新成交量 local symbol = ARGV[1] local new_price = tonumber(ARGV[2]) local new_vol = tonumber(ARGV[3]) -- 读取该行情源旧的报价数据 local old_val = redis.call('HMGET', KEYS[1], 'price', 'volume') local old_price = tonumber(old_val[1]) or 0 local old_vol = tonumber(old_val[2]) or 0 -- 计算新旧值的贡献差值,做增量更新 local old_contrib = old_price * old_vol local new_contrib = new_price * new_vol local delta_num = new_contrib - old_contrib local delta_den = new_vol - old_vol -- 更新聚合中间值 redis.call('HINCRBYFLOAT', KEYS[2], symbol, delta_num) redis.call('HINCRBYFLOAT', KEYS[3], symbol, delta_den) -- 写入该源最新报价 redis.call('HMSET', KEYS[1], 'price', new_price, 'volume', new_vol) -- 计算最新VWAP,兼容成交量为0的极端场景 local total_num = tonumber(redis.call('HGET', KEYS[2], symbol)) or 0 local total_den = tonumber(redis.call('HGET', KEYS[3], symbol)) or 0 local vwap = 0 if total_den > 0 then vwap = total_num / total_den end redis.call('HSET', KEYS[4], symbol, vwap) return vwap
Node.js侧调用示例(以ioredis客户端为例):
// 预加载脚本生成sha值,后续用evalsha调用减少网络开销 const scriptSha = await redis.script('LOAD', luaScriptContent) // 收到Source1的XYZ最新报价 price=101, volume=10200时调用 const latestVwap = await redis.evalsha( scriptSha, 4, 'XYZ:Source1', 'agg:vwap:numerator', 'agg:vwap:denominator', 'agg:vwap:result', 'XYZ', 101, 10200 ) console.log('XYZ最新VWAP:', latestVwap)
3. 多写入端适配方案
如果存在多个业务方无法统一收口写入入口,不需要强制所有写入方改调用Lua脚本,可以通过以下两种方式实现无侵入计算:
- Redis 7.0+版本:将上述计算逻辑注册为Redis Function,配合键空间触发器,直接在Redis内部监听单源报价键的Hash写入事件,自动触发增量计算,完全不需要应用层参与
- 低版本Redis:开启键空间通知(配置
notify-keyspace-events Kh),部署一个极轻量的消费进程,仅接收报价变更事件、不拉取全量数据,收到事件后触发增量计算逻辑即可,网络开销可以忽略
首次上线初始化时,只需要执行一次全量遍历脚本,在Redis侧计算出所有品种的初始聚合中间值即可,后续所有更新全程走增量逻辑,不需要再扫描全量键。初始化过程同样不需要把全量数据传回应用层。
方案优势
- 零全量传输开销:不需要拉取任意品种下的所有行情源数据到应用层,单条更新的网络传输量仅为几个参数的大小
- 性能稳定:增量计算为O(1)复杂度,单品种挂载上百个行情源也不会增加计算耗时,单Redis节点可轻松支撑10w+ QPS的报价更新
- 一致性强:Lua/Redis Function为原子执行,不存在并发更新导致的计算结果错乱
- 结果复用性强:所有业务需要获取VWAP时,直接执行
HGET agg:vwap:result 品种ID即可,不需要重复计算
内容的提问来源于stack exchange,提问作者PirateApp
相关产品推荐
相关产品推荐

