☰
Redis实战:门店营业状态设置的高频读方案与坑点解析
2026/10/2 3:03:34 网站建设 项目流程

这段时间正好在整理Redis学习笔记,又赶上门店营业状态设置这个需求落地,两件事撞到了一块儿。很多做业务开发的同事一提到Redis,第一反应就是“缓存”,再深入一点能说出String、Hash这些数据类型,但真到了营业状态这种看似简单的场景,反而会纠结:这东西到底该存Redis还是MySQL?切换状态要不要上分布式锁?状态变更怎么通知到其他服务?这些问题我在这次实操里都过了一遍,今天就把这份学习笔记和落地过程一起整理出来,给同样踩在这些坑上的朋友做个参考。

营业状态设置这个业务,本质上是高频读、低频写、要求实时可见——用户进门店列表、点商家详情、确认能否下单,每一次都要查状态;而商家改营业状态一天最多几十次。这种数据放MySQL当然也能做,但扛不住秒级响应的实时性要求,更扛不住大促时的读流量。Redis恰好就是为这种场景设计的:状态写入内存,查询走缓存,再配上过期时间、发布订阅、分布式锁,整套方案做下来比想象中要顺,但也确实有不少细节容易翻车。这篇文章不会只停留在“怎么用”,我会把Redis的核心概念、营业状态的数据模型设计、完整实现步骤、以及安装配置和常见坑都拆开讲,适合刚学Redis的开发者,也适合正在做类似业务改造的朋友直接抄作业。

1. 从“营业状态”这个需求聊起

1.1 为什么我会把这个需求和Redis绑在一起

先还原一下业务背景。我们这边是连锁门店的商家后台,每个门店都有开工、打烊、休息中、暂停营业这么几种状态。商家在后台点一下“开工”,用户端的门店列表立刻要能看到“营业中”的标识;点一下“打烊”,所有入口的下单按钮要马上置灰。一开始用的是MySQL状态字段,商家切换后前端轮询接口,接口每次都要查库,高峰期门店列表接口一秒被刷几千次,每个请求都要带一次状态字段的SQL查询,虽然加了索引,压力依然不小。

后来复盘这个场景,有几个特点非常鲜明。第一,状态数据总量很小,无非是几千个门店,每个门店一个状态字符串;第二,读流量远超写流量,用户在逛,商家在操作,读和写的比例可能是几百比一;第三,状态变更要求秒级甚至毫秒级可见,不能接受数据库主从同步的延迟。这几个特点合在一起,几乎就是Redis的标准使用场景。我当时在笔记里给自己画了一条线:MySQL放账单、订单、门店档案这种强一致数据,Redis放营业状态、公告、活动开关这种“短小高频”的实时数据。营业状态这种需求,如果继续堆在MySQL上,等于拿卡车运一袋米,笨重且浪费。

1.2 营业状态管理的核心痛点

真动手做的时候,发现这个看起来“只是改一个字段”的需求,背后其实有五个绕不开的问题。

第一个是实时性。商家点完开关,前端要尽快看到结果,这里就涉及到Redis缓存与后端接口之间的配合,不能做太重的缓存,否则状态切了前端不感知。第二个是一致性。状态存在Redis里,但门店的营业时间表、临时休业计划可能还留在MySQL里,两边必须有一套同步机制,不然就会出现“Redis显示营业中,数据库说已打烊”的幺蛾子。第三个是并发。商家可能在小程序端和后台同时操作,两个请求同时改状态,后写的把先写的覆盖了,或者状态被改成脏数据,这就是典型的并发写问题。第四个是通知。状态变了以后,需要通知到商品服务、订单服务、搜索服务,各服务都得知道这个门店现在可不可下单,最简单的方式就是Redis的发布订阅。第五个是自动恢复。有些门店是“临时休业两小时”,到点自动恢复营业,这就要用到Redis的过期时间或者定时任务回写。

这几个痛点,恰好把Redis的数据类型、过期策略、分布式锁、发布订阅这几块基础知识串起来了。所以我这篇笔记的顺序也是这么走的:先补最核心的概念,再回到业务场景里把每个痛点逐个击破。

2. Redis补课:学明白这几个核心概念再动手

2.1 数据类型:String、Hash、Set、ZSet、Pub/Sub一张表看懂

我的学习笔记第一页就是数据类型,但记了不等于会用。实际写代码的时候,不同业务场景选错类型的例子我见过太多了,这里用一个熟悉的生活场景来类比,理解起来会快很多。

  • String(字符串):Redis里最基础的类型,可以存字符串、数字、二进制数据。比如门店当前状态可以存成open、closed,就像电灯开关,就两个值。它支持SET、GET,还支持INCR、DECR这类原子自增减操作。
  • Hash(哈希):相当于一个对象,一个key下面有多个field-value。比如门店信息可以存成shop:info:{shopId},里面放name、status、business_hours等字段,比塞进一个庞大的JSON字符串更省内存,也更好修改单个字段。
  • List(列表):像排队叫号,左侧进右侧出,适合做消息队列订阅、最新公告列表。
  • Set(集合):无序、不重复。可以存“当前所有营业中的门店ID”,天然去重,还能做并集、交集、差集运算。查“哪些门店既营业又是连锁品牌”这种问题,一条命令就出结果。
  • ZSet(有序集合):在Set基础上带一个分数,可以按分数排序。比如管理“距离自动打烊还剩多少秒”的门店,每次按剩余时间排序,到期的拉出来处理,非常顺手。
  • Pub/Sub(发布订阅):就是广播。一个服务发布消息,其他订阅过的服务都收到通知,我这次做状态变更通知用的就是它。

做营业状态设置的时候,最常用到的是String、Set和Pub/Sub。String存单个门店状态,Set做批量查询“哪些店还在营业”,Pub/Sub做状态变更广播。这三个组合起来,业务上九成需求都能覆盖。

2.2 过期时间与缓存治理:为什么Redis里的数据会“消失”

很多人写Redis代码,只会SET不会EXPIRE,这在大厂面试里基本要被扣分,更重要的是在业务上会埋雷。我自己的笔记里专门记了一条:凡是缓存,必须有失效策略,否则就是“无限膨胀的内存垃圾”。

Redis里的过期时间有几种玩法。

第一种是主动过期。对key执行EXPIRE key seconds,时间到了之后这个key会被删除。比如我要做一个“临时休业两小时自动恢复”的功能,就可以在商家设置临时休业时,给状态key设置一个7200秒的过期时间,到期后key消失,系统再根据兜底逻辑恢复营业状态。

第二种是惰性过期。key过期了但不立即删,等下次访问这个key的时候再发现过期、再删掉。这种机制的好处是省CPU,坏处是过期key如果一直没人访问,会占着内存不释放。

第三种是定期清理。Redis后台会定时的扫描一部分过期的key,把它们清掉,算是主动清理和惰性清理的一种平衡策略。

实际业务里,缓存治理的核心问题不是“过期时间怎么设”,而是缓存和数据库的数据怎么保持一致。这个后面我会专门开一节讲,这里先记住一句话:不要试图让Redis替代数据库,它只是数据库上面的一层加速带,加速带会磨损,必须定期校准。

2.3 分布式锁:多台机器抢一个“营业开关”时怎么办

如果是单机应用,改状态直接用Redisson或者Synchronized就能锁住,但现在的服务基本都是多实例部署,商家请求打到A机器,另一个请求打到B机器,两个实例同时改同一家店的营业状态,MySQL有事务兜底,Redis可没有。

这时候就需要分布式锁。Redis实现分布式锁的经典姿势是SET lock_key unique_value NX EX 30。NX表示只有key不存在时才能设置成功,EX 30代表锁在30秒后自动过期,防止持锁的机器宕机导致死锁。解锁的时候要用Lua脚本比对唯一标识,避免误删别人持有的锁。

营业状态切换这种操作,并发量其实不高,但频率再低也有“客户端重复点击”这种事故场景。商家连点两次“打烊”,两个请求同时进来,没有锁的话状态可能是先设置成打烊又设置成营业,用户体验极差。加了锁以后,第一个请求拿到锁完成切换,第二个请求要么等待要么直接返回“正在切换中”,业务就稳了。

2.4 序列化与可视化:排坑第一步

刚接触Redis的同事,最容易栽在“代码里存进去的值,用客户端一看是乱码”这个问题上。原因很简单——Java的RedisTemplate默认用的序列化器是JDK序列化,存进去的对象是带一堆\xAC\xED前缀的二进制,RedisDesktopManager这种可视化工具打开当然没法看。

所以我的笔记里专门留了一页写序列化方案。通用的做法是统一使用JSON序列化器,比如GenericJackson2JsonRedisSerializer,value存成可读的JSON字符串,方便排查问题。但是要注意:如果你用StringRedisTemplate,那所有value都是字符串,RedisCli视角看起来最直观。我的建议是:业务调试期尽量用StringRedisTemplate直接存字符串,上线后再评估是否引入对象序列化。可观测性永远比花哨的序列化方式更重要。

可视化工具方面,我踩过不少坑。RedisDesktopManager老版本还好用,后来分叉出收费版和免费版,新用户一不留神就下载成付费版本。社区里现在口碑比较好的替代品是Another Redis Desktop Manager(简称ARDM),开源的,跨平台,界面和交互都比较符合习惯;官方还有一个RedisInsight,功能全、支持集群管理,就是启动稍重。Windows用户注意,用Docker或者WSL跑Redis的情况下,客户端连接时要填对端口和密码,两个工具的操作差别不大。

3. 营业状态设置的设计与实现

3.1 数据模型设计:到底该用哪种Redis结构

这是整个实战里最核心的决策点。我当时拿着需求和Redis的数据结构清单,在草稿纸上对比了很久,最后决定用三层设计。

第一层,也是最基础的,用String存每个门店的当前状态。KEY的规范是shop:status:{shopId},VALUE是约定的状态枚举:open(营业中)、closed(已打烊)、rest(休息中)、busy(繁忙)。为什么不用Hash?因为状态是一个高频读写的单字段,Hash虽然能顺带存其他信息,但会让每次读多解析几个字段,性能反而下降,而且String天然支持过期时间,Hash对单个字段做过期需要额外结构,没必要。

第二层,用Set存“当前营业中门店集合”,KEY是shop:open:ids,里面是所有状态为open的门店ID。这个集合解决的是“批量查询”问题。门店列表接口几十个门店,如果挨个查String状态,要几十次网络请求;用Set先拿到营业中ID集合,再配合一次哈希批量取值,就能把几十次请求压缩到一两次。

第三层,用Pub/Sub做状态变更通知,这是后话,3.5节细讲。

设计好之后我还在旁边批了一句话:结构不是越复杂越好,能满足需求的最小结构才是好结构。有些架构师恨不得一个状态用上五种结构,维护起来全是坑。

3.2 状态切换流程:开门、打烊、自动打烊

营业状态切换一共有三种典型操作,对应的处理逻辑完全不同。

第一种是手动开门。商家点击“开始营业”,后端要做四件事:把shop:status:{shopId}设置为open;把shopId追加进shop:open:ids这个Set里;给状态key设置合理的过期时间作为兜底(比如超过营业时间自动失效);通过Pub/Sub发送一条“门店开业”的消息。这里有个细节,手动开门之前要检查门店是否在临时休业状态,如果有冲突要给出提示,不能直接覆盖。

第二种是手动打烊。操作相对简单:设置状态为closed,把门店ID从shop:open:ids里移除,发布一条“门店打烊”消息。需要注意的是,有的业务要求打烊后保留收尾时间,比如“停止接单但允许顾客取餐到十点”,这种情况不能直接删状态,建议再加一个shop:closing_time:{shopId}的字段记录收尾截止时间,用户端根据这个时间显示不同的提示文案。

第三种是自动打烊/自动恢复。这个完全依赖Redis的过期时间。商家选了“临时休业2小时”,那就设置状态key的TTL为7200秒。到期后key自动过期,此时需要一个兜底任务来恢复状态。最简单的方式是用Spring的定时任务每分钟扫描一次即将过期的门店,把数据恢复到默认状态;更高效的方式是用ZSet存“临时休业截止时间戳”,定时任务只取到期的那一批处理,避免全量扫描。我两个方案都搭过,数据量小的时候每分钟全量扫描其实也没事,数据量大了建议上ZSet。

3.3 状态查询与批量查询的演进

查询接口是流量最大的地方,我把演进过程写出来供参考。

第一版是最朴素的单查。前端传一个门店ID,后端GET shop:status:{shopId},返回状态。缺点非常明显:门店列表页一次返回几十家店,前端要循环调几十次后端接口,慢且浪费连接。

第二版做了批量查询。使用MGET命令,一次把几十个状态key全部取回来,Java里用redisTemplate.opsForValue().multiGet(keys)就能实现,往返次数从几十次降到一次。这个版本在门店数量不超过几百家的时候完全够用,但如果是全城搜索“所有营业中的店”,MGET也没辙,你根本不知道key有哪些。

第三版就上了Set集合。商家开店时把ID放进shop:open:ids,打烊时移出去。这样“查询所有营业中门店”就变成两步:SMEMBERS shop:open:ids拿到ID集合,再MGET批量取状态做二次校验。注意这里为什么要二次校验?因为Set集合里的ID和String状态可能存在极短时间的不一致(比如先改状态后删Set),用MGET兜底校验,返回前把不一致的数据修正,能最大程度保证接口数据的正确性。

(这里不用图,实战中把这段逻辑画成时序图挂在自己的文档里,下一节我用文字把时序展开。)

3.4 状态切换接口:用分布式锁兜住并发

写切换接口之前,我先问了自己一个问题:商家连点两次开关,最坏会发生什么?没有锁的话,两个请求同时读到“打烊”,同时改成“营业”,最后一个请求的写入会把前一个覆盖掉。表面上看状态没坏,但如果商家是想“打烊”,结果因为双击变成了“营业”呢?这种Bug一旦出现,用户投诉能打到爆。

所以我在状态切换接口里加了分布式锁,加锁的KEY就用门店维度:lock:shop:status:{shopId}。流程是:

  1. 尝试加锁,用SET lock:shop:status:{shopId} {requestId} NX EX 10,requestId用UUID生成,保证解锁时是自己加的锁。
  2. 加锁失败,说明有另一个请求正在切换,直接返回“操作过于频繁,请稍后再试”。
  3. 加锁成功,执行状态更新、Set维护、发布消息这一整套动作,全部完成后执行Lua脚本释放锁。

这里有个经典问题,为什么不用SETNX长期持有锁,而是要加过期时间?因为如果持锁的机器在执行中途宕机了,锁会一直被占着,其他请求永远拿不到锁。加上过期时间就是“就算你挂了,我最多等10秒就能恢复”,这个思想在面试里叫“兜底机制”,在业务里叫“防死锁”。

至于删除锁为什么要用Lua脚本,是因为“判断value是否等于自己的requestId”和“删除key”这两个动作必须原子,如果分开执行,刚好在判断完之后锁过期了,另一个请求抢占了锁,这时候删除就会误删别人的锁。我笔记里这句话是大写的:千万不要用先GET再DEL的方式解分布式锁。

3.5 状态变更通知:Pub/Sub与消息队列的取舍

状态改完之后,最怕的是服务之间各改各的,谁也不通知谁。我第一次做的时候踩过这个坑,商家的状态在商家服务里改了,但商品服务不知道,导致用户看到门店“营业中”,点进菜单却提示“本店已打烊”。

这里我用Redis的Pub/Sub做了一版轻量级通知。商家服务在状态变化后,向频道channel:shop:status发布一条JSON消息,里面带着shopId、fromStatus、toStatus、updateTime。商品服务、订单服务、搜索服务各自订阅这个频道,收到消息后更新自己本地缓存里的状态。整体响应速度毫秒级,非常直观。

不过我也得说实话,Pub/Sub有个硬伤——消息是即发即弃的,订阅的服务挂了就收不到消息。这时候就必须上消息队列了。营业状态变更这种场景,如果你们公司有RabbitMQ或者RocketMQ,我更推荐直接用专业MQ来做通知,因为MQ有持久化和重试机制,可靠性高一个量级。Pub/Sub更适合“通知丢了也无所谓,反正下次查询会校正”的轻量场景。我当时是因为项目里MQ链路还没打通,先用Pub/Sub顶着,后来接上MQ以后又花时间改了重发逻辑,这里建议后来者直接一步到位。

3.6 与MySQL数据的一致性策略

状态存Redis,营业时间表留MySQL,那两边怎么对账?这是整个设计里最容易被人质疑的点,但也是最容易答好的点。

我的策略是MySQL为主,Redis为加速。商家设置营业状态时,先更新MySQL的shop_status_record表(记录状态和时间戳),然后再更新Redis。如果Redis更新失败,MySQL里状态已经变了,系统靠定时任务把MySQL的状态反向同步回Redis,保证最终一致。这里不追求强一致,因为营业状态丢几秒在业务上可接受。

具体落地有两条路。一条是定时全量对账,每分钟把MySQL里的所有门店状态扫描一遍,和Redis比对,不一致的以MySQL为准回写Redis;另一条是监听MySQL的binlog,状态变更时binlog会触发回调,由回调去更新Redis,比定时任务更实时,但需要引入canal之类的组件,复杂度高。我最终选了定时任务方案,简单、可控、容易排查,数据量几千家门店完全做得过来。

4. 环境准备与工具选型:从安装到可视化

4.1 Linux、macOS、Windows平台的Redis安装

Redis的安装,平台不同差别很大,这里把三个主流平台的方案和我实际用到的命令整理一下。

Linux(CentOS/Rocky系):

# 方式一:直接yum安装(EPEL源里有时版本不够新) sudo yum install redis # 方式二:编译安装,可控性高 wget https://download.redis.io/releases/redis-7.2.4.tar.gz tar xzf redis-7.2.4.tar.gz cd redis-7.2.4 make && sudo make install

我个人更推荐编译安装,虽然多花两分钟,但能自己选版本,还能加上--enable-threads这些编译参数。

macOS:

# 最省事的是brew brew install redis brew services start redis

macOS上原来还有一类人会去搞redis-server源码编译,其实没必要,brew装完以后配置文件在/opt/homebrew/etc/redis.conf(M芯片路径),需要改密码直接改这个文件就行。

Windows:这里要重点说,Redis官方其实没有正式的Windows原生版本,网上流传的所谓“Windows版Redis”很多是第三方的旧版本移植。我对Windows用户有两个建议:首选用WSL2,Windows 10/11的子系统里装Linux后跑原生Redis,性能好、社区方案成熟;次选用Docker Desktop拉Redis镜像跑容器,前提是Docker Desktop能正常启动。如果非要原生exe,可以考虑Memurai这个Windows原生Redis兼容实现,但企业商用要注意授权。

4.2 Docker部署与主从搭建

Docker是最干净的安装方式,配置文件好管理,环境隔离也无敌。拉镜像跑起来的命令非常简单:

docker run -d --name my-redis \ -p 6379:6379 \ -v /my/conf/redis.conf:/etc/redis/redis.conf \ redis:7.2 redis-server /etc/redis/redis.conf

主从复制(主从)的搭建思路也不复杂。准备两份配置,主库正常启动,从库配置里加一行replicaof 192.168.1.10 6379,注意旧版本配置里这个参数叫slaveof。主从模式最大的价值是读写分离和容灾备份,主库写、从库读,主库挂了至少还有从库顶上。但在营业状态这个场景里,我建议还是读写都走主库,因为状态数据太小了,从库读反而要多一次网络跳转,延迟更高。主从更多是放在应用系统的订单缓存这些大读场景下使用。

4.3 可视化客户端怎么选

可视化工具我最终留了两个:Another Redis Desktop Manager(ARDM),免费开源,跨平台,日常开发调试非常顺手,支持批量查看key、命令行、慢日志,还支持暗黑主题;RedisInsight(官方),功能更完整,有集群拓扑图、内存分析、性能分析,团队排查线上问题更好用。

选工具这事我多说一句:不要装一堆客户端,最后发现只用来看一眼key。我见过不少同事电脑里装了五六个Redis客户端,其实一个ARDM完全够用。可视化工具只是一个入口,真正排查问题还是要靠Redis的命令行,比如redis-cli --bigkeys分析大key,redis-cli monitor实时看命令,这些是客户端界面替代不了的。

4.4 Windows下设置Redis密码的两种方式

热词里很多人搜“Windows设置Redis密码”,这里单独拿出来讲,其实是两种改法。

方式一,修改配置文件。在redis.conf里找到requirepass这一行,去掉注释改成requirepass yourstrongpassword,然后重启Redis进程。Docker部署的话,命令是:

docker run -d --name my-redis -p 6379:6379 redis:7.2 redis-server --requirepass yourstrongpassword

方式二,命令行动态设置:

redis-cli > CONFIG SET requirepass yourstrongpassword

注意这种方式是临时生效的,Redis重启以后密码就丢了。生产环境一定要用配置文件方式。

设置完密码后,客户端连接要指定密码:

redis-cli -h 127.0.0.1 -p 6379 -a yourstrongpassword

这里有个安全隐患,-a参数会把密码暴露在进程列表里,生产环境建议用REDISCLI_AUTH环境变量,或者在连接后再执行AUTH命令。我笔记里专门标注了:不要在命令行参数里裸奔密码,尤其是多人在同一台服务器上共用的场景。

5. 常见问题与排查实录

5.1 Docker命令返回500错误的路由问题

很多人装完Docker Desktop以后,执行docker search redis或者docker pull redis会碰到类似“request returned 500 Internal Server Error for API route and version ... check if the server supports the requested API version”这种报错。我帮同事排查过好几次,基本就三类原因。

第一类,Docker Desktop的引擎和客户端版本不匹配。这类报错里经常出现/pipe/dockerDesktopLinuxEngine这种命名管道地址,说明客户端试图访问引擎的管道,但引擎还没就绪或者版本对不上。解决方法是先重启Docker Desktop,打开面板确认左下角的Engine状态是Running,再重新执行命令。

第二类,Docker引擎假死。这时候docker version通常能返回,但真正执行docker search就报错。可以打开Docker Desktop的Troubleshoot,点Restart彻底重启引擎。Windows上偶尔还需要在“服务”里重启com.docker.service这个系统服务。

第三类,老版本Docker Desktop和Windows系统更新之间的兼容问题。这种只能升级Docker Desktop到最新版,或者回退到之前稳定版本。我的经验是,不要盲目追新,选一个已经验证过两个月以上的版本最稳。

5.2 数据“缓存穿透”:门店ID查不到怎么办

正常情况下状态key都会存在,但总有意外:门店ID传错、Redis执行了FLUSHALL、缓存过期后还没回源。这时候如果接口直接返回“该门店不存在”,对客户端来说就是一个巨大的逻辑漏洞。

我的规避手段有两个。一个是对不存在的key也设置一个空值的短TTL,比如PLACEHOLDER占位符,过期时间30秒,防止恶意遍历接口时每次都穿透到MySQL;第二个是在代码里判断如果状态key不存在,立即查MySQL兜底,查到就回填Redis再返回,查不到就返回业务错误。营业状态这种读多写少的场景,缓存穿透是头号威胁,必须做兜底。

5.3 Redis INCR不准?聊聊并发与原子性

热词里有个“redis incr不准”,这个我必须说一说,因为它把很多人绕懵了。INCR命令本身是原子的,单线程执行,不存在“计数丢失”的问题。那为什么不准确?

最常见的原因是业务层的读改写。比如你想实现“同时营业的门店超过100家就拒绝新门店开业”,刚学Redis的人可能会这么写:

int count = redisTemplate.opsForValue().get("open_count"); if (count < 100) { redisTemplate.opsForValue().increment("open_count"); }

这个代码在高并发下必出问题——两个请求同时读到count=99,两个都判断小于100,两个都执行increment,结果计数变成了101,但只有100家店的业务限制被击穿了。这不是INCR不准,是读改写逻辑不是原子的。

正确做法是用Lua脚本把判断和自增放到Redis服务端原子执行:

local count = redis.call('get', KEYS[1]) if tonumber(count) < tonumber(ARGV[1]) then return redis.call('incr', KEYS[1]) else return -1 end

脚本一到Redis那边,就是单线程一口气跑完,不存在并发穿插。我这次营业状态里用到INCR的场景是做“同时开业门店数实时大盘”,用了Lua脚本以后,数据才真正稳定下来。

5.4 从营业状态场景看Redis面试高频题

整理学习笔记的时候,我发现很多Redis面试题都能用营业状态的场景来解释,背题不如实战理解深刻。

“Redis为什么快?”因为它单线程执行命令,避免了线程切换和锁竞争,核心操作全在内存,搭配IO多路复用(epoll)高效处理网络连接。营业状态下几千次查询还是毫秒级响应,靠的就是这套设计。

“Redis持久化怎么选?”RDB是快照,适合恢复和备份,但可能丢最后一次快照后的数据;AOF是操作日志,更实时,但文件大、重启恢复慢。营业状态这个场景,Redis里状态丢了可以从MySQL恢复,所以我会开AOF做安全兜底,但不会把AOF刷盘频率调到最极端,避免性能损耗。

“缓存雪崩、击穿、穿透怎么解决?”雪崩是大批量key同时过期,解决办法是过期时间加随机抖动;击穿是热点key刚好过期,几百个请求同时打到数据库,解决办法是互斥锁重建或者逻辑过期;穿透就是我上面说的查不存在的key,解决办法是空值缓存加布隆过滤器。营业状态里,最容易发生的是单个超级头部门店的击穿,我在代码里给状态查询加了互斥锁,宁可让请求等一下,也不能压垮数据库。

“缓存和数据库一致性怎么保证?”这个问题我在3.6节已经写了实战方案,核心思路是MySQL为主、Redis为加速,最终一致而非强一致,面试官就会觉得你不是背概念的。

5.5 缓存治理:过期时间该设多长

很多人觉得Redis过期的设置是“拍脑袋”,其实是可以计算推导的。营业状态这个场景我设的过期时间是24小时,参数配在配置中心,随时能调。为什么选24小时?因为状态必须随时反映实时营业情况,超过一天没更新的状态本身就是脏数据,应该自动过期兜底。

过期时间的设置有一个通用公式可以参考:过期时间要略大于业务允许的最长数据延迟。如果业务要求用户看到的状态最多慢10秒,那缓存就不能设5分钟,否则状态变了前端要等5分钟才能看到;如果业务能接受1分钟的延迟,那缓存放90秒完全没问题,还能扛住比MySQL多几十倍的流量。总之一定要先定义业务容忍度,再定参数,千万别反过来。

我的缓存治理清单里还专门加了一条:给所有缓存key命名加版本号。比如营业状态的是shop:status:v1:{shopId},后续如果状态枚举增加一个“休息中”的取值,需要强制刷新缓存,只需要把key前缀改成v2,旧缓存自动作废,不用一个个去删。这个思路在发布上线的时候帮了我大忙。

5.6 一个让我从入门到熟练的命令行技巧

最后分享一个命令行技巧,不算问题排查,但很实用。排查Redis的慢查询用SLOWLOG GET 10,看最近的10条慢命令和执行时间;查大key分布用redis-cli --bigkeys,它会扫描全部key,按类型统计最大的几个,我的门店状态缓存曾经因为误存了整个门店JSON对象,瞬间变成几百KB的大key,全靠这个命令找出来的。

如果怀疑某个key的过期时间设置不对,用redis-cli TTL shop:status:001,返回值如果是-1代表永久有效,-2代表key不存在,正数代表剩余秒数。这个命令让我在排查“门店一直显示营业中”问题的时候,一眼看出是程序里没设过期时间,开发小哥还一脸委屈地说“我没写EXPIRE啊”——对啊,问题就是出在“没写”上。

写在最后:这套方案还能怎么扩展

我自己的体会是,营业状态设置看起来是个小需求,但把Redis的数据类型、过期策略、分布式锁、发布订阅、缓存治理全串了一遍,学到的比单独刷十篇面试题都扎实。现在这套方案还能往前再走几步:比如把门店营业状态接入公司的监控大盘,实时统计“当前营业率”;比如把状态变更事件接入工单系统,门店异常打烊自动报警;再比如把临时休业时长累计下来,用来分析门店运营规律。

如果你也在做类似的需求,我的建议就一条:先想清楚数据结构,再写代码,最后调参数。结构对了,后面所有事情都顺;结构错了,后面写多少代码都是在还债。这次实践里我踩过的最大的坑就是一开始图省事全用String硬扛,结果批量查询和自动恢复都实现得很别扭,后来推翻重来才理顺。希望这份学习笔记能帮你少走一段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询