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来维护,性能提升很明显。
3.2 List:天然的消息队列
List在交易场景里,最常用的就是做消息队列。你想想看,交易系统里到处都是事件流——订单来了、成交了、撤单了,这些都需要按顺序处理。
Redis的List是双向链表,左边推右边拉,完美契合生产者-消费者模式。
# 生产者:推送一条订单消息
LPUSH order:queue "{'order_id': '20240301', 'symbol': '600519', 'price': 1895}"
# 消费者:阻塞式获取消息(超时5秒)
BRPOP order:queue 5
我记得有一次做回测系统,需要把历史行情按时间顺序喂给策略引擎。用List做缓冲队列,左边推入历史数据,右边弹出给策略消费,延迟控制在微秒级。比用RabbitMQ轻量太多了。
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
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
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,并发冲突的概率高很多。
HGETALL的延迟会上升。如果对象字段超过100个,考虑拆分成多个Hash。
3.6 实战中的数据结构选择策略
说了这么多,你可能会问:到底该怎么选?我总结了一张表,方便你对照着用。
| 数据结构 | 适用场景 | 不适用场景 | 内存效率 |
|---|---|---|---|
| String | 单一值、计数器、简单缓存 | 复杂对象、需要部分更新的数据 | 高 |
| List | 消息队列、事件流、日志缓冲 | 需要随机访问的场景 | 中 |
| Set | 去重、集合运算、标签系统 | 需要排序的场景 | 中 |
| Sorted Set | 排行榜、订单簿、优先级队列 | 数据量极大且不需要排序的场景 | 低(每个元素额外存score) |
| Hash | 结构化对象、配置信息、账户数据 | 字段数量极多的场景 | 高(小对象有压缩优化) |
3.7 本章小结
这五种数据结构,每个都有自己最擅长的领域。String管简单值,List管队列,Set管去重,Sorted Set管排序,Hash管对象。在交易系统里,我通常会把它们组合起来用——比如用Hash存订单详情,用Sorted Set维护订单队列,用String做计数器。
嗯,最后说一句:数据结构选对了,系统性能就成功了一半。别急着写代码,先想清楚你的数据长什么样,需要怎么访问,再决定用哪个结构。这个习惯,能帮你省下后面无数的重构时间。
公众号:蓝海资料掘金营,微信deep3321