2. Redis核心数据结构:String、List、Hash、Set、Sorted Set在订单簿、行情缓存中的典型用法
说实话,Redis这五种数据结构,我用了快十年了。每次带新人,我都会跟他们说:别急着写代码,先想清楚数据长什么样,该用哪种结构存。选对了,性能翻倍;选错了,后面全是坑。
今天咱们就聊聊,在交易系统里,这五种结构到底怎么用。我会结合订单簿和行情缓存这两个场景,把我踩过的坑、积累的经验,一次性说清楚。
核心观点:数据结构的选择,决定了系统的吞吐上限。选String还是Hash,用List还是Sorted Set,背后是对数据访问模式的深刻理解。
2.1 String:最基础,也最容易被滥用
String是Redis里最简单的结构。存一个key,对应一个value。value可以是字符串、数字,甚至是序列化的对象。
在交易系统里,我主要用它来存:
- 单个行情快照:比如某只股票的最新价、涨跌幅。key用"stock:price:600519",value就是JSON序列化的行情数据。
- 计数器:比如今日成交笔数、实时在线用户数。用INCR命令,一行代码搞定。
- 分布式锁:用SETNX实现抢单锁、撤单锁。这个后面会专门讲。
我的经验:千万别把大对象塞进String里。我曾经见过有人把整个订单簿(几千条数据)序列化后存成一个String。每次更新都要全量读写,性能惨不忍睹。String适合存小数据,比如几百字节以内。
# 存一个行情快照
SET stock:price:600519 '{"price": 185.20, "change": 2.15, "volume": 12345678}'
# 计数器:今日成交笔数
INCR trade:count:20240115
# 分布式锁:抢单时加锁
SETNX order:lock:order123 "locked"
2.2 List:用来做消息队列,但要注意阻塞
List是个双向链表。可以从左边推,右边弹。我最常用的场景是:行情推送的缓冲队列。
比如,行情网关收到交易所的tick数据后,先塞进Redis List里。下游的行情处理程序,用BRPOP阻塞读取。这样能削峰填谷,防止瞬间流量打垮后端。
注意:List的BRPOP是阻塞的。如果消费者挂了,队列会越积越多。我建议配合监控,设置队列长度的告警阈值。曾经有一次,消费者进程OOM了,队列积压了上百万条消息,恢复后瞬间把数据库打爆了。
# 生产者:推送tick数据
LPUSH tick:queue:sh600519 '{"time": "09:30:01.123", "price": 185.20, "volume": 100}'
# 消费者:阻塞读取
BRPOP tick:queue:sh600519 0
另外,List也可以用来做最近N笔成交的缓存。用LTRIM截断,只保留最新的1000条。这样查询最新成交时,不用查数据库。
2.3 Hash:订单簿的绝配
Hash结构,说白了就是一个key下面挂一堆field-value对。这简直就是为订单簿量身定做的。
订单簿长什么样? 一个价格对应一个委托量。比如买一价185.20元,有1000股;买二价185.19元,有500股。用Hash来存,再合适不过了。
# 买盘(Bid)订单簿
HSET orderbook:sh600519:bid 185.20 1000
HSET orderbook:sh600519:bid 185.19 500
HSET orderbook:sh600519:bid 185.18 200
# 卖盘(Ask)订单簿
HSET orderbook:sh600519:ask 185.21 800
HSET orderbook:sh600519:ask 185.22 600
为什么用Hash?因为更新快。订单簿的变化,无非是某个价格上的量变了。用Hash,只需要更新一个field,O(1)复杂度。如果用String存整个订单簿,每次更新都要反序列化、修改、再序列化,慢太多了。
避坑指南:Hash的field数量别太多。我建议一个Hash存一个档位(比如买一到买十)。如果整个订单簿有上百个档位,拆成多个Hash,或者用Sorted Set。否则,hgetall一次拉取所有field,数据量大了会卡住。
2.4 Set:去重和集合运算的好手
Set是无序集合,元素唯一。在交易系统里,我主要用它来做:
- 自选股列表:每个用户一个Set,存他关注的股票代码。用SADD添加,SMEMBERS查询。
- 黑名单/白名单:比如限制交易的股票列表。用SISMEMBER判断某只股票是否在名单里。
- 去重:比如记录今天已经处理过的订单ID,防止重复处理。
# 用户1001的自选股
SADD watchlist:1001 sh600519 sh000001 sz000858
# 判断是否在黑名单中
SISMEMBER blacklist:stock sh600519
# 记录已处理的订单
SADD processed:orders:20240115 order12345
Set还有个好处:可以做交集、并集、差集。比如,找出同时关注了茅台和五粮液的用户,用SINTER一下就出来了。这在做精准推送时很有用。
2.5 Sorted Set:行情排行的利器
Sorted Set(有序集合)是我个人最喜欢的结构。每个元素带一个score,Redis自动按score排序。在交易系统里,它的应用场景太多了。
典型用法一:涨幅排行榜
把股票代码作为元素,涨幅作为score。用ZADD添加,ZREVRANGE取涨幅前10的股票。实时更新,秒级刷新。
# 更新涨幅榜
ZADD ranking:gain:20240115 2.15 sh600519
ZADD ranking:gain:20240115 5.32 sz000858
ZADD ranking:gain:20240115 -1.20 sh000001
# 取涨幅前10
ZREVRANGE ranking:gain:20240115 0 9 WITHSCORES
典型用法二:订单簿的另一种实现
用Sorted Set存订单簿,价格作为score,委托量作为value。这样天然就是按价格排序的。取买一价(最高买价)用ZREVRANGE,取卖一价(最低卖价)用ZRANGE。
# 买盘:价格越高,排名越前
ZADD orderbook:sh600519:bid 185.20 1000
ZADD orderbook:sh600519:bid 185.19 500
# 取买一价(最高价)
ZREVRANGE orderbook:sh600519:bid 0 0 WITHSCORES
我的建议:如果订单簿档位很多(比如超过50档),用Sorted Set比Hash更合适。因为Sorted Set天然支持范围查询,比如取买一到买五,用ZREVRANGE 0 4就搞定了。Hash的话,你得自己排序。
2.6 实战对比:什么时候用哪种?
我整理了一张表,方便你快速决策:
| 场景 | 推荐结构 | 原因 |
|---|---|---|
| 单个行情快照 | String | 数据小,读写简单 |
| 行情推送队列 | List | 支持阻塞读取,削峰填谷 |
| 订单簿(档位少) | Hash | 更新快,O(1)复杂度 |
| 订单簿(档位多) | Sorted Set | 天然排序,支持范围查询 |
| 自选股/黑名单 | Set | 去重,集合运算方便 |
| 涨幅排行榜 | Sorted Set | 按score排序,取TopN极快 |
| 分布式锁 | String | SETNX原子操作 |
2.7 一张图看懂全貌
下面这张SVG图,把五种数据结构在交易系统里的典型用法串起来了。你可以看到,从行情接入、订单簿维护,到排行榜展示,Redis贯穿了整个链路。
2.8 我的几点忠告
最后,分享几个我用Redis做交易系统缓存时的心得:
- 别把所有鸡蛋放一个篮子里:Redis虽然快,但它毕竟是内存数据库。如果数据量太大,内存会爆。我建议只缓存热数据,比如最近1小时的行情、前100档的订单簿。
- 注意过期时间:给缓存设置合理的TTL。比如行情快照,30秒过期就够了。订单簿可以长一点,比如5分钟。这样能自动清理冷数据。
- 监控要到位:Redis的慢查询、内存使用率、连接数,这些指标必须盯着。我曾经因为没监控到内存碎片率飙升,导致Redis频繁swap,整个交易系统延迟从1ms飙到了100ms。
- 选对持久化策略:交易数据不能丢。我建议用AOF + 每秒刷盘。RDB快照也开着,但频率别太高,比如每小时一次。
总结一下:String、List、Hash、Set、Sorted Set,这五种结构各有各的脾气。用对了,它们就是你的神兵利器;用错了,就是你的噩梦。多想想你的数据是怎么读写的,选最自然的那种结构。嗯,就这么简单。