3、Redis核心数据结构:String、List、Set、Sorted Set、Hash 在交易场景中的应用

聊到Redis,很多人第一反应就是「缓存」。但在交易系统里,Redis的定位远不止缓存这么简单。我个人的习惯是,把它当作一个内存数据中间件来用。说白了,交易场景对延迟极度敏感,而Redis的数据结构恰好能帮我们解决很多实时计算和状态管理的问题。

今天我就把这五种核心数据结构,结合交易场景,一个个拆开来讲。每个结构我都会给出实际用法和避坑经验。

3.1 String:最基础,也最容易被低估

String是Redis里最简单的结构,但千万别小看它。在交易系统中,我经常用它来存单一标的的瞬时状态

典型场景: 存储某只股票的当前价格、账户的实时余额、或者一个订单的当前状态。

举个例子,我们有个行情推送服务,每秒会收到几千笔价格更新。如果用数据库去更新,延迟根本扛不住。我的做法是:

# 设置股票当前价格(单位:分,避免浮点数)
SET stock:600519:price 189500

# 读取价格
GET stock:600519:price

# 原子自增(比如处理成交量的累加)
INCR stock:600519:volume

这里有个坑,我必须要说。我曾经在项目里直接用String存JSON序列化后的订单对象,结果每次更新都要先GET再SET,并发一高就出问题。后来我改成只存关键字段,用多个Key来维护,性能提升很明显。

我的建议: String适合存单一值,别把整个对象塞进去。如果对象字段多,考虑用Hash。

3.2 List:天然的消息队列

List在交易场景里,最常用的就是做消息队列。你想想看,交易系统里到处都是事件流——订单来了、成交了、撤单了,这些都需要按顺序处理。

Redis的List是双向链表,左边推右边拉,完美契合生产者-消费者模式。

# 生产者:推送一条订单消息
LPUSH order:queue "{'order_id': '20240301', 'symbol': '600519', 'price': 1895}"

# 消费者:阻塞式获取消息(超时5秒)
BRPOP order:queue 5

我记得有一次做回测系统,需要把历史行情按时间顺序喂给策略引擎。用List做缓冲队列,左边推入历史数据,右边弹出给策略消费,延迟控制在微秒级。比用RabbitMQ轻量太多了。

注意: List做队列时,如果消费者挂了,消息会堆积。建议配合LLEN监控队列长度,超过阈值就报警。

3.3 Set:去重与集合运算的利器

Set的特点是无序、不重复。在交易系统里,我主要用它来做去重集合运算

举个实际例子:风控系统需要检查某个股票是否在「黑名单」里。用Set存黑名单,查询复杂度是O(1),比数据库快得多。

# 添加黑名单股票
SADD blacklist:stocks 600519 000001 300750

# 检查是否在黑名单中
SISMEMBER blacklist:stocks 600519

# 获取所有黑名单股票
SMEMBERS blacklist:stocks

还有一个场景我特别喜欢——计算两个策略的持仓交集。比如策略A和策略B都持有了哪些股票,用Set的SINTER命令一秒就算出来了。

# 策略A持仓
SADD portfolio:A 600519 000001 300750
# 策略B持仓
SADD portfolio:B 600519 000002 300750

# 求交集(共同持仓)
SINTER portfolio:A portfolio:B
# 结果:600519, 300750
避坑指南: 我曾经用Set存订单ID做去重,结果订单量太大,Set占了几百MB内存。后来改用Bloom Filter,内存省了90%。Set适合小规模去重,大规模场景要考虑内存开销。

3.4 Sorted Set:交易系统的「排序神器」

Sorted Set(有序集合)是我在交易系统里用得最多的结构。它每个元素都有一个分数(score),Redis自动按分数排序。这简直就是为排行榜、优先级队列、时间序列量身定做的。

最经典的场景——订单簿的买卖盘口。买盘按价格从高到低排,卖盘按价格从低到高排。用Sorted Set实现,代码量少得惊人。

# 卖盘:价格越低优先级越高(score用价格)
ZADD orderbook:ask 1895.0 "order_001"
ZADD orderbook:ask 1896.0 "order_002"
ZADD orderbook:ask 1894.5 "order_003"

# 获取最优卖价(最低价格)
ZRANGE orderbook:ask 0 0 WITHSCORES

# 获取卖盘前5档
ZRANGE orderbook:ask 0 4 WITHSCORES

另一个我常用的场景是实时波动率排名。把每只股票过去5分钟的波动率算出来,存到Sorted Set里,分数就是波动率值。然后随时可以拉出「波动率最高的前10只股票」。

# 更新波动率
ZADD volatility:5min 0.035 "600519"
ZADD volatility:5min 0.028 "000001"
ZADD volatility:5min 0.042 "300750"

# 获取波动率最高的3只
ZREVRANGE volatility:5min 0 2 WITHSCORES
核心优势: 插入、删除、查询都是O(log N)复杂度。在几千只股票的排名场景下,性能完全够用。

3.5 Hash:对象存储的最佳选择

Hash结构,说白了就是一个字段-值的映射表。在交易系统里,我拿它来存结构化对象——比如账户信息、订单详情、持仓数据。

为什么不用String存JSON?因为Hash支持单独更新某个字段,不需要整个对象序列化反序列化。这在高频交易里差别很大。

# 存储一个订单的多个字段
HSET order:20240301:001 status "pending" price 1895 volume 100 symbol "600519"

# 只更新状态字段
HSET order:20240301:001 status "filled"

# 获取所有字段
HGETALL order:20240301:001

# 获取单个字段
HGET order:20240301:001 price

我记得有个项目,需要实时更新账户的「可用余额」和「冻结余额」。用Hash存,每次成交后只更新对应的字段,其他字段不受影响。如果用String,得先GET整个JSON,改完再SET,并发冲突的概率高很多。

我的经验: Hash的字段数量不要太多,建议控制在100个以内。字段太多时,HGETALL的延迟会上升。如果对象字段超过100个,考虑拆分成多个Hash。

3.6 实战中的数据结构选择策略

说了这么多,你可能会问:到底该怎么选?我总结了一张表,方便你对照着用。

数据结构 适用场景 不适用场景 内存效率
String 单一值、计数器、简单缓存 复杂对象、需要部分更新的数据
List 消息队列、事件流、日志缓冲 需要随机访问的场景
Set 去重、集合运算、标签系统 需要排序的场景
Sorted Set 排行榜、订单簿、优先级队列 数据量极大且不需要排序的场景 低(每个元素额外存score)
Hash 结构化对象、配置信息、账户数据 字段数量极多的场景 高(小对象有压缩优化)
重要提醒: 别为了用Redis而用Redis。如果数据量超过内存容量,或者需要复杂查询(比如多条件过滤),还是老老实实用数据库。Redis是加速器,不是万能药。

3.7 本章小结

这五种数据结构,每个都有自己最擅长的领域。String管简单值,List管队列,Set管去重,Sorted Set管排序,Hash管对象。在交易系统里,我通常会把它们组合起来用——比如用Hash存订单详情,用Sorted Set维护订单队列,用String做计数器。

嗯,最后说一句:数据结构选对了,系统性能就成功了一半。别急着写代码,先想清楚你的数据长什么样,需要怎么访问,再决定用哪个结构。这个习惯,能帮你省下后面无数的重构时间。

Redis核心数据结构在交易场景中的应用 Redis 数据结构 String 股票价格、账户余额 计数器、简单缓存 List 消息队列、事件流 日志缓冲 Set 黑名单、去重 集合运算 Sorted Set 订单簿、排行榜 波动率排名 Hash 订单详情、账户信息 持仓数据 选择策略 • 单一值/计数器 → String • 消息队列/事件流 → List • 去重/集合运算 → Set • 排序/排行榜 → Sorted Set • 结构化对象 → Hash 组合使用效果更佳,例如:Hash存订单 + Sorted Set维护队列

公众号:蓝海资料掘金营,微信deep3321