公司动态
Redis十大数据类型深度解析:从缓存到数据结构服务器的实战指南
1. 从“键值对”到“十大数据类型”Redis的进化之路提到Redis很多人的第一反应就是“缓存”。没错它凭借其内存级的读写速度和简单的键值对模型成为了缓存界的“扛把子”。但如果你对Redis的认知还停留在简单的set key value和get key那可能就错过了它最强大的部分。Redis之所以能从一个简单的缓存中间件进化成如今被广泛认可的“数据结构服务器”其核心就在于它提供的丰富数据类型。这不仅仅是五个、八个而是整整十种内置的数据结构类型。每一种类型都不是凭空设计而是为了解决特定场景下的数据建模难题将原本需要在应用层通过复杂代码和多次查询才能实现的功能内化为了原子性的命令操作。我见过不少项目初期为了图省事把所有数据都序列化成JSON字符串然后一股脑地用String类型存进Redis。上线初期相安无事随着业务增长各种奇葩需求就来了“给这个用户的积分排个名”、“统计一下最近一周登录过的用户”、“把这个商品列表按价格排序分页给我”……这时候面对一堆字符串要么在应用层做繁重的计算拖慢整体性能要么频繁与数据库交互失去了缓存的意义。这就是没有用对数据类型的典型困境。Redis的十大数据类型就是十种应对特定场景的“武器”。理解它们本质上是在学习如何根据数据的访问模式来选择合适的存储结构从而用最少的资源最高效地满足业务需求。这不仅仅是API调用的问题更是一种数据建模和系统设计的思维。接下来我就结合自己这些年踩过的坑和总结的经验带你彻底搞懂这十种类型看看它们各自在什么场景下能发挥出最大威力。2. 基石类型String字符串的深度解析与实战误区String是Redis最基本的数据类型也是其他所有类型的基石。但千万别小看它“简单”往往意味着灵活和高效。一个Redis字符串value最多可以是512MB这足以存储一张不小的图片序列化后的数据或一篇长文章。### 2.1 不只是文本String的二进制安全与多功能命令String是二进制安全的。这意味着它可以存储任何数据比如序列化的对象、图片的字节流而不仅仅是文本。其核心命令SET、GET、DEL大家都很熟悉但它的威力远不止于此。原子计数器这是String最经典的应用之一。INCR、DECR、INCRBY这些命令是原子操作在高并发场景下无需加锁即可实现精准计数用于文章阅读量、用户点赞数、库存扣减等场景再合适不过。SET article:1001:views 0 INCR article:1001:views # 阅读量1并发安全批量操作与过期管理MSET/MGET用于批量操作减少网络开销。SETEX、PSETEX可以在设值的同时设置过期时间秒/毫秒级是实现缓存失效的标配。位操作BitMap这是String类型一个隐藏的“大招”。通过SETBIT、GETBIT、BITCOUNT、BITOP等命令你可以把String当作一个巨大的位数组来操作。每个用户ID对应一个偏移量只需一个比特位就能标记其状态如是否签到、是否在线极大地节省了内存。# 记录用户uid1001在2023-10-01是否签到假设该日期偏移量为365 SETBIT sign:2023:10:01 1001 1 # 检查是否签到 GETBIT sign:2023:10:01 1001 # 统计当天签到总人数 BITCOUNT sign:2023:10:01### 2.2 实战避坑滥用String与序列化陷阱String虽然万能但滥用就是最大的坑。误区一万物皆可JSON String。这是最常见的反模式。比如存储一个用户信息{“name”: “张三”, “age”: 30}用String存储后如果你想修改年龄必须GET回来在应用层反序列化、修改、再序列化、最后SET回去。这个过程涉及网络IO和序列化开销且非原子性。更合适的做法是使用Hash类型。误区二忽略过期时间导致内存泄漏。用Redis作缓存一定要设置合理的过期时间TTL。我曾经排查过一个线上问题Redis内存持续增长直至写满触发淘汰原因就是大量缓存键未设置TTL变成了“永久缓存”。使用EXPIRE命令或直接在SET时用EX/PX选项是必须养成的习惯。误区三大Key问题。如果一个String的value非常大比如几百KB甚至上MB在对其进行GET、SET甚至过期删除时会导致主线程阻塞影响其他命令的执行。对于大文本或大对象需要考虑是否应该拆分或者使用其他更适合的结构。String是入口但绝非终点。当你发现自己在用String存储结构化数据或需要复杂操作时就该考虑下面这些更专业的类型了。3. 结构化数据存储首选Hash哈希的精细化管理之道当你的数据是一个对象Object拥有多个字段Field时Hash类型就是为你量身定做的。它类似于编程语言中的MapString, String适合存储一个实体的多个属性比如用户、商品、配置项。### 3.1 Hash的核心优势原子操作与内存效率与将整个对象序列化成String相比Hash有两个压倒性优势原子性字段操作你可以单独对某个字段进行HSET、HGET、HINCRBY而无需读写整个对象。这对于频繁更新部分字段的场景如更新用户最后登录时间、增加商品库存性能提升巨大且是原子性的。内存优化Redis的Hash在元素较少时采用一种称为ziplist压缩列表的紧凑编码方式比将每个字段分开存储成独立的String键要节省大量内存。这对于存储海量小对象如用户会话、商品SKU属性至关重要。# 存储一个用户信息 HSET user:1001 name “张三” age 30 city “北京” # 单独获取年龄 HGET user:1001 age # 原子性地增加年龄 HINCRBY user:1001 age 1 # 获取所有字段和值 HGETALL user:1001### 3.2 场景对比与使用边界Hash并非没有缺点。HGETALL命令在字段非常多时会返回一个巨大的回复可能阻塞客户端。此时可以使用HSCAN进行渐进式遍历。另外Hash不支持直接对内部的多个field进行范围查询或排序这是它的设计边界。那么何时用String何时用Hash用String数据是一个不可分割的整体读写总是以整个单位进行如缓存一个完整的HTML页面、一个序列化的配置对象。或者需要用到String特有的位图、计数器功能。用Hash数据是结构化的你需要频繁地、独立地访问或修改其中的部分字段。典型场景就是各种“对象”的缓存用户会话Session、商品信息、文章元数据等。一个经验法则是如果你在应用层代码里经常需要从一个大JSON字符串中解析出某个特定字段那么是时候改用Hash了。4. 有序集合与列表List与Sorted Set的排序艺术当数据需要体现顺序时List和Sorted Set就该登场了。它们都维护着元素的顺序但实现方式和适用场景截然不同。### 4.1 List列表灵活的双端队列与消息队列Redis的List是一个双向链表这意味着在头部和尾部进行插入删除操作的时间复杂度是O(1)非常高效。它的核心能力是序列和排队。消息队列简易版利用LPUSH生产消息和BRPOP阻塞式消费消息可以构建一个简单的消息队列。BRPOP在没有元素时会阻塞连接避免了消费者轮询的空转消耗。# 生产者 LPUSH myqueue “task1” # 消费者阻塞等待超时时间5秒 BRPOP myqueue 5注意这只是一个基础模型。对于需要ACK、重试、死信队列等复杂功能的场景建议使用专业的消息中间件如RabbitMQ或Kafka。Redis Stream类型后面会讲到也是一个更强大的选择。最新列表LPUSHLTRIM是一个经典组合可以轻松实现一个固定长度的最新动态列表比如网站的最新100条评论。LPUSH latest:comments “评论C” LTRIM latest:comments 0 99 # 永远只保留最新的100条实战坑点List的索引访问LINDEX效率是O(N)性能随列表长度线性下降切忌用它来实现随机访问。它的优势在于头尾操作和范围获取LRANGE。### 4.2 Sorted Set有序集合排行榜与范围查询的利器如果说List是按插入顺序排序那么Sorted Set就是按一个显式的分数Score来排序。它是Redis数据类型中最具特色的之一结合了Set的去重性和按分数排序的能力。核心数据结构每个元素都是一个成员Member- 分数Score对。成员唯一分数用于排序双精度浮点数。分数可以相同此时按成员的字典序排序。排行榜实现这是Sorted Set的“杀手级”应用。无论是游戏积分榜、商品销量榜还是热门文章列表都能轻松应对。ZADD leaderboard 95 “Alice” 87 “Bob” 95 “Charlie” ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前三名降序 ZRANK leaderboard “Bob” # 获取Bob的排名升序排名范围查询Range Query你可以高效地获取分数在某个区间内的所有成员ZRANGEBYSCORE。这可以用来实现诸如“查找价格在100到200之间的所有商品ID”、“获取昨天下午3点到5点所有在线用户”等功能。集合运算ZUNIONSTORE、ZINTERSTORE可以对多个有序集合进行并集、交集计算并将结果存为新集合。例如可以计算同时出现在“一周热门”和“本月热门”两个榜单中的文章。Sorted Set的内部实现是跳跃表SkipList和哈希表的结合保证了范围查询和单点查询的高效。它的一个高级用法是将时间戳作为分数成员作为事件ID这样就可以构建一个时间轴或延迟队列。5. 去重与集合运算Set的基础与高级玩法Set集合是一个无序的、元素唯一的容器。它的基础应用是去重但它的集合运算能力才是真正的价值所在。### 5.1 基础去重与随机元素去重快速判断一个元素是否存在SISMEMBER或存储一个不重复的集合如文章的标签、用户的所有好友ID。SADD article:1001:tags “数据库” “缓存” “Redis” SISMEMBER article:1001:tags “缓存” # 返回1表示存在随机抽取SRANDMEMBER或SPOP弹出命令非常适合实现抽奖、随机推荐等场景。例如从所有参与活动的用户中随机抽取10名幸运者。# 假设 sadd activity:users user1 user2 ... user10000 SRANDMEMBER activity:users 10 # 抽取10个不重复的用户不删除 SPOP activity:users 10 # 抽取10个用户并从集合中移除### 5.2 强大的集合运算社交关系与数据过滤Set的SINTER交集、SUNION并集、SDIFF差集命令为复杂的数据关系查询提供了原子性的解决方案。共同关注/好友在社交网络中查找A和B的共同好友就是计算两个用户好友集合的交集。SINTER friends:user:A friends:user:B兴趣标签推荐计算拥有相似标签的用户。可以先找出与目标用户标签集合交集最大的其他用户。数据过滤假设有一个“黑名单IP”集合和一个“今日访问IP”集合SDIFF可以快速找出不在黑名单中的访问IP。这些运算在服务端原子性完成避免了在应用层进行多次查询和循环比对性能极高。但需要注意当参与运算的集合非常大时这些命令可能会比较耗时在阻塞Redis主线程。对于大数据集可以考虑使用SSCAN迭代并结合客户端计算或者将结果缓存起来。6. 地理空间与基数统计Geo与HyperLogLog的专项突破Redis在后续版本中引入了更专门化的数据类型用于解决特定领域的高频问题。### 6.1 Geo地理空间索引附近的人与地点搜索Geo本质上是使用Sorted SetZSET的一种特殊封装将二维的地理坐标经纬度通过Geohash算法编码成一维的分数Score从而利用ZSET的有序特性实现附近位置的查询。核心命令GEOADD添加地理位置GEODIST计算两点距离GEORADIUS/GEORADIUSBYMEMBER查询指定半径内的元素。GEOADD restaurants 116.404 39.915 “全聚德” 116.408 39.920 “东来顺” GEORADIUS restaurants 116.405 39.915 5 km WITHDIST # 查找5公里内的餐厅并返回距离实现原理理解其基于ZSET实现很重要。这意味着你可以用ZREM删除一个地点用ZRANGE查看所有地点虽然没意义但更重要的是你可以利用ZSET的所有特性。例如给每个地点附带一个“热度”分数然后按距离和热度进行综合查询这需要一些客户端计算。精度与性能Geo的精度对于大多数LBS应用如附近商家、打车已经足够。它的性能远高于在关系数据库中用ST_Distance_Sphere函数进行计算。但要注意数据量极大上千万时范围查询的复杂度是O(NlogM)仍需评估性能。### 6.2 HyperLogLog基数统计海量数据去重计数HyperLogLog是一种概率数据结构用于估算一个集合中不重复元素的数量基数。它的最大优势是占用空间极小且固定。一个HyperLogLog键只需要约12KB内存就能以标准误差小于1%的精度统计接近2^64个不同元素的基数典型场景统计一个大型网站每日的独立访客数UV。如果使用Set来存储每个用户的ID对于亿级用户量内存消耗是灾难性的。而使用HyperLogLog每天只需要12KB。PFADD uv:2023-10-01 “user_id_1” “user_id_2” “user_id_1” # 添加元素自动去重 PFCOUNT uv:2023-10-01 # 估算当天的UV PFMERGE uv:2023-10-week1 uv:2023-10-01 uv:2023-10-02 # 合并多天的数据估算整周的UV重要限制HyperLogLog只提供计数无法获取具体的元素内容也无法判断某个特定元素是否已经添加过。它只回答“大约有多少个不重复的元素”这个问题。对于需要精确去重或获取明细的场景它不适用。实战心得PFADD和PFCOUNT都是非常快的O(1)操作。合并多个HLLPFMERGE也是高效的。它通常用于替代那些“只需要一个大概数字”的Set场景是节省内存的神器。7. 位图与流Bitmap与Stream的扩展应用这两种类型可以看作是基础类型的威力加强版分别扩展了String和List的能力边界。### 7.1 Bitmap位图极致的空间利用如前文在String类型中提到的Bitmap是通过String类型的位操作命令实现的。但它解决的问题如此典型以至于我们常常将其视为一个独立的数据类型。它的核心价值在于用最小的空间表示大量的布尔状态。用户行为标记除了签到还可以用于记录用户是否阅读过某条消息、是否拥有某项权限、是否完成某个新手任务等。每个用户只需要一个比特位。大数据量下的特征筛选假设有1亿用户需要筛选出“女性”且“活跃”的用户。可以创建两个Bitmap一个标记性别一个标记活跃状态。通过BITOP命令对两个位图进行AND运算得到的结果位图中值为1的位对应的用户ID就是目标用户。这个操作的速度极快且内存消耗极小1亿用户约需12.5MB。SETBIT gender:female 1001 1 SETBIT active:20231001 1001 1 BITOP AND result:target gender:female active:20231001 GETBIT result:target 1001 # 结果为1表示用户1001符合条件注意事项Bitmap的偏移量offset是整数。如果你的用户ID不是连续的整数需要建立一个从用户ID到偏移量的映射关系这可能会增加一些复杂度。通常可以使用用户ID的自增主键部分作为偏移量。### 7.2 Stream流完善的消息队列与事件溯源Redis 5.0引入的Stream类型旨在弥补List作为消息队列时的功能缺失提供了一个完整的、支持多消费者组的、可持久化的消息队列解决方案。核心概念消息Stream中的一条记录包含一个唯一的ID通常由时间戳-序列号组成和多个键值对字段。消费者组允许多个消费者共同消费同一个Stream组内消费者负载均衡每条消息只会被组内的一个消费者处理。Pending List已投递给消费者但尚未被确认ACK的消息列表用于处理消费失败后的重试。与List的对比特性List (LPUSH/BRPOP)Stream消息回溯消费后即删除无法重现消息持久化可重复读取多消费者一个消息只能被一个消费者获取支持消费者组组内竞争消费确认机制无弹出即视为成功有显式ACK机制失败可重投阻塞订阅支持支持功能更丰富功能完整性简单完整支持消息ID范围查询、监控等# 生产者添加消息 XADD mystream * user “Alice” action “login” # 创建消费者组 XGROUP CREATE mystream mygroup 0 # 消费者从组内读取消息 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream # 消费者确认消息处理完成 XACK mystream mygroup message-id适用场景Stream非常适合需要可靠消息传递、顺序性、且希望用Redis统一技术栈的场景。例如用户活动追踪Event Sourcing、微服务间的异步通信、日志收集等。它比List更可靠但比Kafka、RabbitMQ等专业消息队列更轻量功能上也有所取舍。8. 概率去重与地理围栏Bloom Filter的客户端实现与GEO进阶除了内置类型Redis还可以通过模块或客户端算法支持更多高级数据结构。其中布隆过滤器的应用尤为广泛。### 8.1 Bloom Filter布隆过滤器存在性校验的守门员Redis自身没有内置Bloom Filter但可以通过RedisBloom模块或直接在客户端利用Bitmap实现。它的作用是以极小的空间代价快速判断一个元素“一定不存在”或“可能存在”于一个超大集合中。工作原理使用多个哈希函数将一个元素映射到位数组Bitmap的多个位置上并将这些位置置为1。查询时如果该元素对应的所有位置都是1则它“可能存在”如果任何一个位置是0则它“一定不存在”。典型应用缓存穿透防护在查询数据库前先用Bloom Filter判断键是否存在。如果Bloom Filter说“不存在”则直接返回空避免对数据库的无效查询。因为Bloom Filter不会漏报不存在的一定会判否所以能有效拦截恶意的不存在Key请求。推荐去重在新闻推荐中判断一篇新文章是否已经推荐给过某个用户。使用Bloom Filter可以快速过滤掉绝大部分已读文章只在“可能存在”的情况下才去查询精确的已读记录库大幅降低查询压力。实现方式服务端模块加载RedisBloom模块后可以使用BF.ADD、BF.EXISTS等命令最为方便。客户端算法Bitmap在应用层实现哈希和位运算将Redis的Bitmap作为存储介质。这种方式更灵活但需要自己维护。重要缺陷Bloom Filter有误判率False Positive即可能将不存在的元素误判为存在。误判率可以通过增加位数组大小和使用更多哈希函数来降低但无法消除。因此它只适用于那些可以接受偶尔误判、但对“不存在”的判断要求绝对准确的场景。### 8.2 GEO的进阶思考距离计算与范围查询的代价虽然Geo用起来很简单但在设计大规模LBS系统时还需要考虑更深层次的问题距离计算负载GEORADIUS命令在查询时需要计算中心点与集合内每个点的距离。当集合内元素数量巨大例如全球所有店铺时即使使用地理哈希预先筛选计算量依然可观。常见的优化策略是分级索引例如先按国家、城市等大范围筛选出一个子集再在这个子集上执行精确的Geo查询。结果排序与分页GEORADIUS默认返回所有结果如果范围内元素很多返回的数据量会很大。虽然可以用COUNT选项限制但分页是个难题因为每次查询的范围是固定的传统的LIMIT offset, count模式在这里不适用除非配合SCAN。一种做法是先获取所有元素的ID和距离在客户端进行排序和分页但这会带来额外的网络和计算开销。动态位置更新对于移动对象如车辆、外卖员位置频繁更新。频繁调用GEOADD更新坐标是可行的但要注意这本质上是ZADD操作。如果对象数量极多更新频率极高可能会对Redis造成写入压力。需要根据业务容忍度适当降低位置更新的频率。理解这些底层细节能帮助你在享受Redis Geo便利的同时提前规避性能瓶颈设计出更健壮的LBS服务。9. 类型选择决策树与混合使用策略面对十种类型如何选择我总结了一个简单的决策流程可以帮你快速定位方向需要存储一个简单的值或计数器吗-String需要存储一个对象多个字段吗-Hash需要维护一个有序的序列且经常从两端操作吗-List需要去重或者做集合运算交集、并集吗-Set需要按某个分数排序或者按分数范围查询吗-Sorted Set需要处理地理位置和附近搜索吗-Geo需要估算海量数据的唯一值数量吗-HyperLogLog需要记录大量的布尔状态是/否吗-Bitmap需要可靠的消息队列或多消费者流处理吗-Stream然而真实的业务场景往往更复杂混合使用多种类型才是高级玩法。例如场景实现一个带点赞计数的文章评论列表并按热度排序。评论列表使用List存储每条评论的IDLPUSH保证时间序。评论内容使用Hash存储评论ID到详细内容作者、正文、时间的映射。点赞数使用String计数器或Hash中的字段键名为comment:点赞数。点赞用户记录防重复点赞使用Set键名为comment:liked_users存储点赞用户ID。评论热度榜使用Sorted Set成员是评论ID分数是点赞数或一个综合热度分点赞数时间衰减。每当有点赞事件更新对应评论ID的分数。通过这种组合你可以高效地实现列表分页获取、单条评论详情读取、点赞的原子操作和热度排序所有操作都在Redis内完成极大地减轻了数据库的压力。10. 性能、内存与持久化类型选择背后的工程考量选择了正确的类型并不意味着万事大吉。在工程实践中你必须关注它们对性能和内存的影响。### 10.1 内存编码的奥秘ziplist, intset, skiplistRedis为了节省内存对小尺寸的数据结构采用了特殊的紧凑编码方式Hash/List/ZSet元素较少时会采用ziplist压缩列表编码将所有元素紧凑地存储在一起。Set元素较少且均为整数时会采用intset整数集合编码。当元素数量或大小超过配置的阈值时它们会转换为标准的hashtable、linkedlist、skiplist等结构。通过OBJECT ENCODING key命令可以查看一个键的内部编码。理解这一点很重要盲目追求“小”可能适得其反。如果你为了利用ziplist而刻意将一个大Hash拆分成无数个tiny hash反而会因为管理大量键的元数据而浪费更多内存。需要根据实际数据规模和访问模式调整redis.conf中如hash-max-ziplist-entries等参数找到最佳平衡点。### 10.2 大Key与热Key的监控与治理大Key通常指value size过大如10KB的String 元素数量5000的Hash/Set/ZSet的Key。大Key会导致DEL命令阻塞、网络传输慢、内存分配不均等问题。可以使用redis-cli --bigkeys扫描或通过MEMORY USAGE命令分析。治理方法包括拆分如将大Hash按字段前缀拆成多个小Hash、压缩客户端压缩value、使用更适合的数据结构如用HyperLogLog替代大Set做基数统计。热Key指访问频率非常高的Key。热Key会造成单实例负载过高成为性能瓶颈。监控可以通过redis-cli --hotkeys需开启maxmemory-policy为LFU或分析慢查询日志。解决方案包括本地缓存如Guava Cache、读写分离、使用Redis Cluster将热Key通过hash tag强制分配到独立slot等。### 10.3 持久化与数据安全数据类型的选择不影响RDB或AOF持久化机制但会影响持久化文件的大小和恢复速度。例如一个包含百万成员的Set其AOF日志文件会记录大量的SADD命令。在极端情况下考虑禁用某些重写成本高昂的命令。更重要的是无论使用何种类型都要理解SAVE/BGSAVERDB快照和appendfsyncAOF刷盘策略的配置根据业务对数据安全性和性能的要求做出权衡。通常建议同时开启RDB和AOF用RDB做冷备用AOF保证数据完整性。11. 从命令到设计数据类型在真实架构中的角色让我们看一个更综合的案例设计一个简易的社交网络“关注/粉丝”与“动态推送”系统。关系存储Set类型存储关注列表和粉丝列表followings:user:{uid}和followers:user:{uid}。SADD/SREM用于添加/取消关注SCARD用于获取数量SINTER用于计算共同关注。动态发布与推送写扩散当用户发布一条动态时除了存入数据库我们执行LPUSH将动态ID写入自己的动态列表posts:user:{uid}。获取自己的所有粉丝ID从followers:user:{uid}。对每个粉丝ID执行LPUSH dynamic:feed:{follower_uid}将动态ID推送到他们的个人Feed流中。这里每个用户的Feed流就是一个List。这种“写扩散”模式读性能极佳直接LRANGE分页但写操作成本高适合粉丝数不多的场景如普通社交。动态拉取读扩散对于粉丝数巨大的大V采用“读扩散”。发布动态时只存入一个全局的Sorted Set中分数为发布时间戳ZADD global:posts timestamp post_id。当用户查看Feed时系统需要获取他的关注列表followings:user:{uid}。对这些关注用户的ID执行ZUNIONSTORE合并他们发布的动态可以预先为每个用户维护一个Sorted Setposts:user:{uid}。从合并后的临时Sorted Set中按分数倒序分页获取动态ID。这种模式写轻读重适合粉丝量巨大的场景但需要更复杂的聚合逻辑且可能用到ZUNIONSTORE这种较重命令。热点动态排行使用一个全局的Sorted Set如hot:posts成员是动态ID分数是热度值点赞数权重 评论数权重 时间衰减。每当有点赞、评论事件就更新对应动态的分数。首页的热榜直接从这个ZSet中获取。在这个案例中Set、List、Sorted Set各司其职共同构建了核心功能。选择“写扩散”还是“读扩散”就是根据数据模型用户粉丝量对读写压力的权衡这比单纯记住命令要重要得多。理解Redis的十大数据类型绝不是背诵API手册而是学习一种用“数据结构服务器”的思维来建模和解决实际问题的能力。从简单的缓存键值到复杂的关系运算、流处理、地理搜索Redis提供了一套丰富而高效的原语。真正的功夫在于如何根据你的数据特征和访问模式像搭积木一样灵活、混合地运用这些类型构建出既快又省的内存数据层。下次当你设计一个功能时不妨先问问自己这个数据在Redis里应该长什么样