ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis五大核心应用场景:缓存、锁、计数器与消息队列深度解析

Redis五大核心应用场景:缓存、锁、计数器与消息队列深度解析 前几天帮一个朋友排查线上问题登上服务器一看那一台Redis节点上真是热闹商品缓存、秒杀库存计数、几把分布式锁、一份异步任务队列还有用户的登录会话。当时我就想这不就是Redis最典型的几种打开方式吗。日常开发里大家总说Redis简单无非get/set可真要用好它首先得想明白一个问题这个东西凭什么能在这么多完全不同的场景里扛事这篇文章就把Redis最高频的五种用途——缓存加速、分布式锁、计数器与限流与排行榜、轻量消息队列、状态共享与去重——逐个拆开结合我实际踩过的一些坑聊点真东西。不管你是刚开始学Redis的后端还是写了几年业务代码的老手应该都能从中找到点有用的东西。1. Redis凭什么是瑞士军刀——先搞清楚能力边界Redis所有扩展能力其实都建立在五种基础数据结构之上。String、Hash、List、Set、ZSet每一种都对应一类真实需求。我在选型时习惯先对着这张表过一遍基本上就能判断该用哪种结构。数据结构底层组织方式典型场景String动态字符串缓存值、计数器INCR/DECR、分布式锁、限流Hash哈希表压缩列表对象属性存储比如用户信息、购物车List双向链表/快速列表消息队列、时间线数据、最新列表Set哈希表去重、抽奖池、关注关系、UV统计ZSet跳表哈希表排行榜、排序需求、滑动窗口限流举个具体的例子为什么Hash适合存对象因为半结构化的对象属性变化很常见今天多一个字段明天少一个字段。如果用String整体覆盖要么序列化整个对象要么为每个字段单独建key前者浪费流量后者管理混乱。Hash一个key下面挂field天然就是一个小Map改address只改address不会牵连其他数据。同理ZSet为什么能做排行榜靠的是score这个维度Redis底层用跳表让插入和范围查询都维持在O(logN)级别几千万条会员积分排名也能毫秒级返回。1.1 单线程、原子命令和内存模型才是所有用途的根基Redis很多特性都源于一个看起来有点过时的设计单线程执行命令。因为它把所有命令串行执行天然避开了多线程竞争所以INCR、LPUSH这类命令根本不需要额外的锁。这也是为什么分布式锁、过期时间能保证原子性——命令在内存里排队执行不会出现两个客户端同时改同一个key的竞态问题。当然单线程也意味着一条慢命令会拖垮整个实例。所以Redis官方一直警告别在生产环境用KEYS别在线上对大集合做SORT、SMEMBERS。本质上这也是对它能力边界的一种尊重够快但不是无所不能。还有一个关键点是内存。Redis的数据全在内存里这是它快得没道理的原因也是它贵得没道理的原因。正因为内存贵我们对待Redis的态度应该是精打细算哪些数据值得放进去、value设计得多大、过期时间设多久都是有讲究的。你把一个5MB的PDF存进去还不如老老实实放磁盘。有了这些基础认知下面这五种用途就好理解了它们全是踩在数据结构原子操作内存访问这三块基石上长出来的。2. 用途一缓存加速真正的分水岭在缓存治理如果说只选一种用途那必须是缓存。十个Redis实例里八个是拿来扛读请求的。2.1 缓存的基本盘不是所有数据都适合缓存缓存的前提是数据存在读多写少、热点集中的特征。商品详情页、用户基本信息、配置数据都很典型。判断标准很简单你拍脑袋想一下线上每秒1000次查询背后大概有多少次写如果写占比低于10%就很适合。但缓存设计的痛点从来不是怎么加缓存而是缓存加完之后系统为什么还是出问题。线上常见的情况是缓存一重启数据库直接被压垮某个热门商品的key刚过期瞬间一堆请求全打到数据库。这些都是缓存治理要解决的事高频出现的三个词分别是穿透、击穿、雪崩。2.2 缓存穿透查一个根本不存在的数据穿透指的是请求绕过了Redis直接打到数据库最典型的就是查询一个不存在的ID。攻击者可以故意构造一批不存在的ID让所有请求全部穿透到数据库数据库瞬间被打挂Redis反而成了摆设。常规解法有两条路。第一条是空值缓存数据库查不到数据时也往Redis里放一个空值并设置一个较短的过期时间比如30秒。这样同一个不存在的ID在30秒内不会再次打到数据库。第二条是布隆过滤器预先用一个比较小的bitmap把所有合法ID装进去查询前先判断这个ID是不是可能存在如果布隆过滤器说不在直接返回连Redis都不用查。布隆过滤器有个特点说不在一定不在说在不一定真在。所以它是用来挡确定性的非法请求不能当作精确判断。如果用Java可以引入Guava的BloomFilter也可以在系统启动时把所有存量ID预加载进去增量ID实时加入。我实际项目里两种方案都配合过入口层用布隆过滤器挡非法IDRedis层用空值缓存兜少量误判请求数据库压力直接降了两个数量级。注意布隆过滤器的误判率是可以调的位数组越大、哈希函数越多误判率越低但内存消耗也越大。一般把误判率设置在1%左右性价比最高。2.3 缓存击穿热点key过期瞬间并发全部打到DB击穿和穿透最大的区别是穿透查的是不存在的数据击穿查的是存在但刚好过期的热点数据。比如双十一的热门商品详情页缓存设置10分钟过期一到时间没有任何缓冲上万个请求同时查到这个key已失效全部涌向数据库。处理击穿常用的套路有三板斧互斥锁重建缓存时只让一个线程干活。其他线程拿到锁失败后短暂sleep再重试避免同时回源。热点逻辑过期value里额外存一个逻辑过期时间字段读到时发现逻辑过期返回旧值的同时启动一个异步线程去刷新缓存。核心思想是永远不真正让缓存失效。延长热点key的过期时间配合定时任务在业务低峰期统一续期甚至对标识为hot的商品设置永不过期由后台任务维护更新。互斥锁的含义是查Cache未命中时尝试去Redis拿一把重建缓存的锁拿到锁的人去查库、回填缓存、释放锁没拿到锁的人在本地等待后重试。注意这里说的锁和后面要讲的分布式锁是同一套东西所以怎么用对锁很关键。我印象很深的一次事故是某个团队为了省事直接写死Thread.sleep(1000)结果高峰期几千个线程在本地排队等锁接口平均RT直接飙到3秒。互斥锁方案一定要控制重试次数和等待时间宁可让少量请求快速失败返回也别把线程全部拖住。2.4 缓存雪崩大面积key同时过期Redis形同虚设雪崩是击穿的放大版——不是某一个key出问题而是一大批key在同一时间段集体失效。比如系统初始化的缓存过期时间全部写死成1小时每到整点就会迎来一波回源峰值恰好赶上流量高峰时就是一场事故。最简单的预防手段就是过期时间打散在基础过期时间上增加一个随机偏移比如3600random(600)秒。这招成本极低但能有效避免整点雪崩。再往上就是多级缓存兜底本地缓存CaffeineRedis、熔断降级、数据库限流。真到了数据库撑不住的时候宁可返回旧的缓存数据、返回降级页面也不能让后端存储被打死。2.5 一致性维护先更新数据库再删缓存的经典配置缓存和数据库的一致性是缓存治理里最绕不开的一环。我一般直接采用业界共识度最高的方案写请求先更新数据库成功后删除Redis缓存读请求未命中缓存时再从数据库回填。为什么不更新Redis而是删除因为更新动作本身可能产生中间态两次并发写库很容易把缓存写脏而删除缓存让下次读自动回填简单、安全、不会产生脏数据。当然删缓存也可能遇到窗口期删除缓存后、回填前面的短暂时间里读请求还是会打到数据库。这里有一个进阶操作叫延迟双删先删除缓存更新数据库完成后再延迟几百毫秒删除一次。用意是防止删除缓存时刚好有请求在回填旧数据第二次删除能把这个偶发窗口堵住。需要注意延迟双删也不是百分百可靠对一致性要求极高的场景正经做法是引入消息队列或监听binlog来异步删缓存这里就不展开细讲了。3. 用途二分布式锁——看似简单坑全在细节里分布式锁是Redis被提到第二多的用途也是面试和线上事故的重灾区。3.1 单体时代的锁为什么失效了以前单机应用用Synchronized、ReentrantLock就能保证并发安全因为所有线程都在同一个进程里锁是进程级的。一旦应用拆成多个实例、前面挂了负载均衡一个请求被分到A机器另一个请求被分到B机器各自进程里的锁谁也管不了谁。这时候就需要一把所有实例都能看到的锁。Redis天然就是那个所有实例连着的公共节点这就是分布式锁的由来。3.2 从SET NX PX到Lua脚本手写一把能用的锁分布式锁最简单的实现就一条命令SET key value NX PX 3000。NX表示只有key不存在时才设置成功PX表示过期时间value必须是一个唯一标识比如UUID这是为了解锁时确认这把锁确实是我加的。加锁代码很简单String lockKey order:pay:12345; String lockValue UUID.randomUUID().toString(); Boolean locked stringRedisTemplate .opsForValue() .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);需要小心的恰恰是解锁。很多人图省事直接DEL这是致命的——万一锁的value已经变成另一个线程的了比如自己这把锁超时过期了别的线程拿到锁DELETE会把别人的锁删掉锁就形同虚设。所以解锁必须校验value而且要保证校验删除两步是原子的。标准写法是用Lua脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end用Java执行时String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; stringRedisTemplate.execute( new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), lockValue);这套组合能跑但有一个问题写死30秒业务逻辑复杂一点数据库慢查询一来锁提前过期了另一个线程进来等于又没锁了。3.3 生产环境的更优解Redisson与看门狗真要上生产我建议直接用Redisson。它把上面这套锁逻辑封装好了默认加锁成功后启动一个后台定时任务也就是俗称的看门狗每隔一段自动续期。默认锁超时是30秒每10秒续一次只要线程没结束锁就不会被自动过期误杀。代码极简RLock lock redissonClient.getLock(order:pay:12345); if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } }注意tryLock有两种写法tryLock(waitTime, TimeUnit)只传等待时间和单位默认启用看门狗续期tryLock(waitTime, leaseTime, TimeUnit)显式指定租期后看门狗就不会生效锁到期自动释放。想用续期就别传leaseTime。唯一要提防的是在锁里做重IO操作。看门狗会续期意味着锁可以被无限续期重度操作长时间占锁其他请求就一直排队这比死锁还难受。3.4 一个真实事故Redis命令超时Lettuce报错时锁该怎么办说到锁的可靠性我印象很深的一次事故跟Redis命令超时有关。线上Spring Boot服务偶发报错Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。排查路径很有代表性第一步看异常栈。这种报错十有八九是Lettuce客户端执行命令超时。默认命令超时时间是60秒但如果连接池满了命令排队太久也会超时。第二步用redis-cli --bigkeys扫描实例果然发现几个大key——正好是某业务塞进去的大List和Hash单个key已经几十MB。想更直观的话用RedisInsight或者Another Redis Desktop Manager这类可视化工具看一眼大key也是一目了然。第三步目标明确了某些慢命令HGETALL、LRANGE 0 -1占用连接时间太长导致连接池被打满锁服务的加锁命令排不上队开始超时。处理方式是四管齐下给大key设置合理上限或者拆分把Lettuce连接池从默认值调大并适当调整命令超时时间比如从60秒改为3秒让快速失败而不是长尾拖垮整个线程池给慢key的访问加上限流对Redis连接做监控告警。这个事故说明一个很现实的问题Redis里如果塞满了各种业务数据某个大key就可能让整台实例的锁服务、缓存服务一起跟着遭殃。如果一台Redis既要承担缓存又要承担锁最好把锁的key和大缓存key放在不同的逻辑分区里或者至少给锁服务单独开一个连接池。否则一次大key查询就能让分布式锁全部失效分布式锁失效在最坏情况下意味着高并发业务会同时进入临界区。关于分布式锁还有一个绕不开的知识点是Redis集群下锁的安全性问题RedLock的争议。如果Redis是主从模式加锁后Master挂了、同步没完成锁就丢了。Redis官方给了一套RedLock算法来应对但业界对其在极端场景下的安全性一直有争论。我的建议是如果业务真的强依赖锁的绝对可靠考虑ZooKeeper或etcd如果Redis能接受极少概率失效那就基于单节点主Redis加好超时再把锁内逻辑压缩到最短——这套组合能覆盖99%以上的线上场景。4. 用途三计数器、限流与排行榜——原子操作撑起的高并发数字业务Redis第三个高频用途是数字游戏计数、限流、排行。这类需求的共同点是高频、高并发、要求原子性Redis的INCR和ZSet在这里几乎无敌。4.1 INCR/DECR库存扣减、点赞数与访问量先看最简单的计数器。INCR key可以让一个整数加1DECR key可以让它减1整个过程在Redis单线程模型下天然原子两万并发同时INCR同一个key也不会出现加减错乱。秒杀库存就是一个经典例子。库存存在Redis里每次扣减Long rest stringRedisTemplate.opsForValue() .decrement(seckill:stock:1001); if (rest 0) { // 扣减成功生成订单 } else { // 库存不足回补本次扣减 stringRedisTemplate.opsForValue().increment(seckill:stock:1001); }这里要注意判断rest大于等于0后如果订单生成失败必须回补。回补也有竞态问题更严谨的做法是用Lua把扣库存检查库存合并成一个原子操作或者在扣减前先查库存足够再DECR。虽然单条命令原子但扣减和生成订单是两条命令之间跨了网络请求你永远要意识到原子边界在哪里。点赞数、浏览数、用户每日积分累计都是同一套路。能用INCR解决的就别用数据库先select再update。万一并发双击点赞还好高并发下数据库行锁能把CPU打满Redis一个INCR干净利落。4.2 滑动窗口限流Redis也能当流量闸门限流也是计数器的一种变形。最简单的是固定窗口限流每来一个请求INCR一次超过阈值就拒绝每秒重置。但它有明显的窗口临界问题第999毫秒和第1001毫秒各来1000个请求固定窗口会在两个独立窗口里各放过1000个实际2秒内放过了2000个。滑动窗口能解决这个问题用ZSet记录窗口内每次请求的时间戳String key sliding:rate:user:123; long now System.currentTimeMillis(); long window 5000L; // 5秒窗口 // 移除窗口外的旧记录 zSetOperations.removeRangeByScore(key, 0, now - window); // 加入当前请求 zSetOperations.add(key, String.valueOf(now), (double) now); // 统计当前窗口内请求数 Long count zSetOperations.zCard(key); if (count 20) { // 超过阈值拒绝 }这套逻辑虽然分三步操作但每一步都是单个命令依然很快。注意给窗口key设置合适的过期时间窗口结束后整key自动消失免得几百个用户的限流key把内存吃干榨净。除了ZSet滑动窗口还有基于List的令牌桶、基于Hash的计数等变体。令牌桶相对更平滑用一个线程或延迟任务定期往Redis里放令牌请求来了先消耗一个令牌令牌不足就返回限流。实际项目中如果限流维度固定、QPS特别高本地限流Caffeine/Guava RateLimiter加Redis全局兜底组合比较合理毕竟所有请求都过RedisRedis本身可能成为瓶颈。4.3 ZSet排行榜排序和排名两种需求一次满足排行榜本质是按分数排序 取TopN 查我的排名正好是ZSet的看家本领。商品销量榜、直播人气榜、积分榜结构都长一个样// 加分 zSetOperations.incrementScore(rank:hot:goods, goodsId, 1); // 取Top10 SetString top10 zSetOperations.reverseRange(rank:hot:goods, 0, 9); // 查某个商品的排名 Long rank zSetOperations.reverseRank(rank:hot:goods, goodsId);注意要用reverse因为排行榜一般取分数最高的正序的话排名越靠前分数越小。底层用跳表维护有序性插入和排序都在O(logN)级别百万级数据量的榜单也没有压力。这里有个小设计经验如果榜单要做小时榜、天榜、周榜我习惯用不同的key后缀去隔离比如rank:hour:20241120到期自然过期不需要手动清数据。还有一个坑score用Double存储如果分数超过2^53约9000万亿会有精度丢失。一般业务达不到这个量级但如果你用时间戳做排序分数要小心13位的毫秒时间戳已经超出Double的精确整数范围。遇到这种情况可以用时间戳业务序号折算成更小的数值或者改用别的排序字段。5. 用途四轻量消息队列与发布订阅——行但别什么都干说到Redis做中间件很多人会皱眉让一个KV存储去当消息队列靠不靠谱分场景。轻量级、对消息不敏感的场景Redis的List队列完全够用而且比引入Kafka轻太多。5.1 List队列LPUSH BRPOP的经典组合List本身就是双向链表天然支持从一个方向写入、从另一个方向消费。生产者LPUSH任务进队消费者BRPOP阻塞取任务// 生产者 listOperations.rightPush(task:email:queue, JSON.toJSONString(emailDTO)); // 消费者阻塞10秒 String taskJson listOperations.leftPop(task:email:queue, 10, TimeUnit.SECONDS);BRPOP/BLPOP的阻塞特性很关键队列为空时消费者不会空转疯狂拉取而是挂起等待既不耗费CPU也不产生无效网络请求。多个消费者同时BRPOP同一个key时Redis会保证每个消息只发给一个消费者天然实现了任务分发。这套方案实际项目中用得很多发邮件、发短信、生成报表、执行异步导入导出都是把耗时操作丢进队列后台Worker慢慢消费。好处是和业务共用同一个Redis不需要额外部署消息队列中间件技术栈和运维成本都最低。5.2 一个真实短板消息可能丢失没有ACK机制但List队列有一个关键短板消费者从队列里取走消息后如果处理失败或者进程崩溃这条消息就永久丢失了。RPOPLPUSH可以缓解——把消息先挪到另一个处理中队列消费成功后再删除崩溃后还能从处理中队列捞回来但这套流程复杂说白了就是自己造轮子。所以我的判断标准是Redis队列适合的场景有三个特征——任务量不大每秒几百条以内、任务内容不是强业务关键、丢失后能接受重试或手工补。如果任务是扣钱下单这种核心链路千万别用Redis List硬扛。5.3 发布订阅广播消息但别指望可靠除了ListRedis还有一个PUBLISH/SUBSCRIBE的发布订阅模型。它和List队列有本质区别List是一个消息一个消费者Pub/Sub是一个消息所有订阅者都收到更多用于实时广播场景比如配置变更通知、消息推送、在线状态同步。Pub/Sub有一个致命特性消息不持久化消费者不在线就收不到。发布者推送时如果订阅者因为断线、宕机不在连接上这条消息直接丢弃。所以它只适合实时在线推送、丢了也没关系的场景。真要想做可靠的消息广播还是上专业队列吧。把话题拉回Redis到底适合当什么中间件。很多架构师其实心里清楚单机Redis做消息队列的吞吐上限就摆在那里List在几十万条消息时还能撑上千万条就明显吃力。我当时在一个统计平台里试过用List存日志收集任务单实例跑到5000 QPS左右CPU就开始告警而同样的量级Kafka几乎无感。所以我的选择思路很简单能用业务Redis顺手解决的用Redis需要持久化、分区、消费组、重放、高吞吐的直接换Kafka/RabbitMQ。Redis做中间件是刚刚好够用的方案别把它硬扛成全能Message Broker。6. 用途五会话共享与去重统计——分布式环境下的状态中心第五种用途很多人没意识到它是分布式多实例场景下状态中心的角色。6.1 多实例部署后的Session共享单体应用时代用户的登录Session存在单个应用服务器的内存里不会有任何问题。一旦应用多实例部署用户在A实例上登录了下一次请求被负载均衡转给B实例B内存里没有这个Session用户又被要求重新登录。体验极差。解法就是用Redis保存Session。按SessionId为keyHash存储用户信息设置过期时间所有实例都从同一个Redis读取// 登录成功 HashOperationsString, String, Object hash redisTemplate.opsForHash(); hash.put(session:user: sessionId, userId, userId); hash.put(session:user: sessionId, nickname, nickname); redisTemplate.expire(session:user: sessionId, 30, TimeUnit.MINUTES); // 校验 Object userId hash.get(session:user: sessionId, userId);这里有一个序列化问题值得单独提醒。用Spring Boot的Redis时如果value序列化配置不对存进去的是对象读出来是乱码或者版本升级后反序列化直接失败。项目大小无所谓但RedisTemplate的序列化器一定要指定。我一般用GenericJackson2JsonRedisSerializer并对放入的Java对象做统一封装尽量避免不同业务模块各自定义DTO互相混放否则线上数据一旦序列化格式变了老数据全读不出来只能清缓存。6.2 Set、Bitmap与签到、UV去重Redis的Set天生支持去重。抽奖活动中将每个参与用户加入SetsAdd(draw:lottery:10001, userId); Boolean joined sIsMember(draw:lottery:10001, userId); long userCount scard(draw:lottery:10001);同一用户只会占一个位置。底层结构是哈希表判断是否已存在是O(1)百万级用户的去重毫秒级完成。类似场景还有判断用户是否点赞过、是否已领取优惠券、黑名单校验全是Set的活。另一个容易被低估的数据结构是Bitmaps本质还是String按位操作。可以用一个bit位代表一个用户ID来做大数据量UV统计。要统计某个商品的访问独立用户数用户ID映射到offset每天一个bitmap key// 访问时置1 stringRedisTemplate.opsForValue().setBit(uv:goods:1001, userIdHash, true); // 统计每天的UV Long uvCount stringRedisTemplate.execute( (connection) - connection.stringCommands().bitCount(uv:goods:1001.getBytes()));以1亿用户为例一个bitmap只需要100000000/8/1024/1024大概12MB内存就能统计整个平台的日活跃用户数。同样的数据量如果存到数据库几亿行的统计表跑一次要几分钟。签到这个场景更典型Bitmaps天然用位偏移代表日期第N位代表第N天用户连续签到直接按位与判断内存占用极小。这里要小心一点bitmap的offset不能太大。如果用户ID本身很大比如雪花ID上亿直接映射到bit位会导致内存浪费需要先做一次哈希收敛到合理范围比如hash后的值对某个上限取模。6.3 别忽略持久化AOF、RDB与主从集群既然把Redis当状态中心你就必须面对一个灵魂拷问Redis挂了数据还在吗纯缓存挂了就挂了重新从DB回填即可但Session、计数器的数据如果丢了会直接影响线上业务。Redis持久化有两个机制RDB快照和AOF追加日志。简单对比一下对比项RDB快照AOF追加日志文件形式二进制快照文本命令追加数据丢失窗口取决于快照周期可能丢分钟级配合everysec最多丢1秒恢复速度快直接加载快照慢需要逐条重放文件体积小大适用场景备份、快速恢复对数据完整性要求高生产环境的常规选择是同时开启RDB负责快速恢复和备份AOF负责兜底最近1秒的数据。再往上就是主从复制和哨兵主库写、从库读主库挂了自动切换从库为新的主库。Redis主从、Redis集群这些部署模式基本都是为解决两件事——提高吞吐量和提高可用性。在这个状态中心的定位下你会遇到一个常见选择题Redis Cluster和主从哨兵怎么选。Cluster能自动分片扩容适合数据量超大、写QPS超高的场景但它的多key操作比如ZSet并集会受分片限制主从哨兵适合数据量可控、需要主从读写分离的场景支持事务和更灵活的命令。选型的核心依据还是看这台Redis到底在为你承担什么角色。顺带说一句我容器部署的经验。很多人习惯用docker search redis找镜像但Docker Desktop偶尔会报docker search redis request returned 500 internal server error这类API版本或镜像源配置的错。这种报错多半是引擎API版本问题直接跳过search用docker pull redis:7.x官方仓库的tag就能绕过去。7. 最后说点我的真实体会说回开头那个线上问题。我最后帮他们把Redis拆成了两个实例一个负责缓存和队列一个专门承担锁和Session顺带把几个大key清掉那串Lettuce超时报错就再也没出现过。回头看Redis的很多线上事故不是因为能力不够而是因为边界没划清楚。一台Redis被当成万能工具箱之后什么场景都往里塞最后哪个场景都没服务好。如果让我给一个刚从缓存入门、开始用Redis的人提建议我会说先只做一件事把缓存这一件事做透穿透、击穿、雪崩、一致性全走一遍再考虑分布式锁等锁踩过几个坑再慢慢扩展队列、计数这些场景。不要第一天就把Redis当成全家桶。Redis真正聪明的用法不是让它承担更多而是让它每一份内存都花在刀刃上。
返回列表