作为一个Java工程师,黑马商城这套项目教程在圈子里名气一直不小,尤其是Redis篇。我以前带团队做电商类项目时,还专门把这里面的缓存方案摘出来改造成生产级配置。这篇内容不是我对着官方文档念经,而是把我自己从零搭建、开发、踩坑到最后部署上线的完整过程捋一遍,重点放在Redis在分布式架构里到底是怎么发挥作用的,以及那些光看视频学不到的细节。无论你是准备跳槽面试,还是手头正好要搞一个高并发的商城系统,这篇都值得花几分钟看完。
很多人刚开始接触这个项目时会犯一个错误:把Redis只当成一个缓存数据库来用,存点商品信息就完事了。实际上黑马商城Redis篇的价值在于它把同一个中间件在不同业务场景下的用法全部串起来了,缓存只是最基础的一层。真正关键的是分布式锁、Session共享、消息队列、数据统计这套组合拳。我接下来会按照从架构设计到落地部署的顺序,把这些核心环节一个个拆开讲清楚。
1. 项目定位与整体架构拆解
1.1 黑马商城为什么要引入Redis
电商类项目有一个典型的共性特点:读多写少。用户浏览商品、查看详情、翻购物车,这些操作占了请求量的绝大部分,而真正的下单支付只是很少一部分。如果所有读取都打到MySQL上,数据库很快就会被拖垮。黑马商城引入Redis,核心目的就是把热点数据的读取压力从数据库上卸下来。
但是这里有个容易忽略的点:Redis做缓存并不是简单地把数据塞进去就万事大吉。你还要考虑缓存什么时候更新、数据不一致怎么办、万一缓存雪崩了怎么兜底。黑马商城这个项目妙就妙在它把这些场景都揉进了具体业务里。比如商品信息查询、店铺信息查询、分类列表这类接口,都是典型的缓存应用场景;而秒杀扣库存、订单幂等性处理,就必须靠Redis的原子性操作和分布式锁来支撑。
我在实际工作中验证过这套思路的可行性。一个日活几十万的商城系统,把我这边Redis缓存层做好之后,MySQL的QPS直接从高峰期的两万多降到了三千左右,响应时间也从平均300毫秒降到了50毫秒以内。这就是为什么很多企业面试时特别看重候选人能不能说清楚缓存、锁、持久化这些细节,因为这些都是线上项目真正会用的东西。
1.2 分布式架构里Redis扮演哪几个角色
先说一个很多人理解不到位的地方:Redis在分布式架构里从来不是“只干一件事”的中间件。在标准的生产环境中,Redis至少同时承担四个角色。
第一个角色是缓存层,这个大家都很熟。商品详情、用户信息、热点榜单这些东西用Redis存一份,请求进来先查Redis,查不到再去查MySQL,然后把结果回填到Redis。第二个角色是分布式锁的载体。高并发场景下多个服务节点同时操作同一份数据,比如秒杀扣减库存,如果不加锁就会出现超卖问题。基于Redisson或者原生SETNX命令实现的锁,就是靠Redis的原子性来保证多节点之间的互斥。
第三个角色是Session存储。传统单体应用把Session放在应用内存里,但分布式架构下用户请求可能被负载均衡到不同的节点上,如果Session不共享,就会出现“登录状态漂移”的问题。把Session扔进Redis,所有节点读同一份数据,问题就迎刃而解。
第四个角色是异步队列。虽然专业的消息队列通常是RabbitMQ或者Kafka,但Redis的List结构配合阻塞命令,完全可以胜任一些轻量级异步任务。黑马商城教程里用Redis实现了订单超时自动取消、异步通知等功能,这种设计在中小型项目里既省去了额外部署消息队列的成本,又能达到解耦的效果。
1.3 项目目录结构与领域划分
黑马商城这个项目的包结构很值得学习,它严格按照领域划分模块,而不是按技术层来分。你去观察controller、service、mapper这些目录会发现,它把用户、商品、订单、支付等业务领域拆得很清晰,每个领域内部再按照web、service、dao的层次往下组织。
这种划分方式在生产环境里有一个明显好处,就是多人协作时不会互相冲突。每个人负责一个独立的领域包,提交代码时的合并冲突概率大幅降低。另外它也为后续的微服务化改造留了后路,哪天你发现订单模块的访问量太大需要拆成独立服务,直接把整个订单包拎出来就行,因为它内部的依赖是自洽的。
在数据库层面项目主要用了MySQL来存储核心业务数据,Redis来做缓存和辅助业务逻辑。要注意的是,项目里的表结构设计充分考虑了索引和查询效率,比如商品表和商品详情表分开设计,用户表和用户地址表分开设计,这些细节在面试官问起数据库优化时都是加分项。
2. 环境准备与工具选型
2.1 本机环境:JDK、Maven与依赖管理
在动手之前先把环境统一好。这个项目基于Spring Boot,所以JDK版本建议直接用8或者11,不要用太新的版本,避免出现兼容性问题。Maven建议使用3.6以上版本,你可以在settings.xml里配置阿里云镜像仓库,不然依赖下载的速度会让你怀疑人生。
依赖管理上项目主要引入了Spring Boot Web、MyBatis Plus、Redis、Redisson这几大块。我建议你打开pom.xml看一下,把这些依赖的版本号理清楚。特别是在Spring Boot 2.3.x和2.7.x之间,Redis连接方式和Redisson的兼容包名都有变化,如果版本对不上很容易出现启动报错。
一个非常实用的经验是:项目里尽量不要混用不同版本的Redis客户端依赖。之前有粉丝私信我,说项目里同时引入了spring-boot-starter-data-redis和redisson-spring-boot-starter,结果因为两者内部的jedis和lettuce版本冲突,导致Redis连接时好时坏,折腾了一天才发现是依赖冲突。统一版本如果不知道怎么处理,直接先把redisson注释掉,调试完缓存部分再打开,这个排查思路会让你少走很多弯路。
2.2 Redis安装的两种方式对比
Redis本身是Linux上的产物,Windows上的安装包其实是别人维护的移植版,版本更新速度会慢一些。如果你用的是Windows本机开发,我建议用两种方式里更省心的那种:直接用Docker跑一个Redis容器。
Docker方式只需要一条命令:docker run -d --name redis -p 6379:6379 redis:6.2,就这么简单。端口映射做完,本机的Java项目直接连接localhost:6379就能用。忘掉Windows安装包里那些复杂的配置和烦人的服务注册,容器方式五分钟就能搞定。
如果你的环境不支持Docker,那就老老实实下载Windows版Redis,解压后执行redis-server.exe启动。要注意的是Windows版默认没有配置密码,监听的又是所有网卡,如果你在公司或者学校网络上跑,很容易被别人扫到端口,有安全风险。建议启动时加上需要认证的配置,或者直接把bind设置成127.0.0.1,只允许本机访问。
Mac用户相对幸运,brew install redis直接装,装完redis-server启动就行,配置文件在/usr/local/etc/redis.conf,需要开启持久化时改这里的appendonly参数。
2.3 可视化客户端与命令行工具
开发调试Redis时,纯命令行也能用,但效率确实不高。我日常开发用的最多的是Redis Desktop Manager,不过这个工具新版开始收费了。开源社区里Another Redis Desktop Manager是它的替代品,界面差不多,支持Windows、Mac、Linux,连接Redis时能看到所有key,还能直接查看各种数据类型的内容,日常排查数据格式问题很够用。
有一点要注意,可视化工具只是用来观察数据状态的,真正处理复杂问题还是离不开命令行。尤其是排查慢查询、查看内存碎片、观察键过期情况这些操作,我建议你还是打开终端敲redis-cli。命令行的info、memory doctor、slowlog get这类命令,能给你提供可视化工具给不了的一手数据。我见过太多人在可视化工具里翻半天找不到原因,结果一条slowlog命令立刻定位问题的案例了。
3. 核心功能模块的开发实战
3.1 商品缓存与缓存更新策略
商品信息是商城系统的核心热点数据。实战里我会在查询逻辑上做一层封装:先从Redis里查,命中就直接返回;没命中就去MySQL里查,查到了就把数据写入Redis并设置过期时间,再返回给前端。
这里最值得说的是缓存更新策略。黑马商城教程里推荐的是Cache Aside模式,简单说就是读的时候先读缓存,读不到就查数据库然后重建缓存;写的时候先更新数据库,再删除缓存。为什么要删除而不是更新缓存?因为更新缓存的操作往往需要做很多字段的拼接和序列化,代价高且容易出错;而删除缓存只是让下一次读取时自然重建,逻辑简单可靠。
实际操作时有个细节坑,就是“先删缓存后更新数据库”和“先更新数据库后删缓存”这两种顺序的差异。后者是主流做法,但依然存在一个时间窗口:线程A更新完数据库但还没来得及删缓存,线程B读到旧缓存就返回了。要彻底解决这个问题,思路是引入消息队列或者在更新完数据库后延迟几百毫秒再删除一次缓存,俗称“延迟双删”。虽然不能做到绝对最终一致,但在绝大多数业务场景下已经足够了。
3.2 缓存穿透、击穿、雪崩的防护
这三个问题是面试必问的,也是线上出事故最多的地方。缓存穿透指的是查询一个根本不存在的key,Redis里没命中,请求直接打到数据库,恶意攻击时可以用不存在的id把数据库打垮。解决方案是缓存空值或者用布隆过滤器先拦截。
缓存击穿是指某个热点key在过期的瞬间,突然涌进大量请求,全部穿透到数据库。解决方案是互斥锁,也就是让同一时刻只有一个线程去数据库加载数据,其他线程等着这个线程写完缓存后再读。实现方式可以用Redis的SETNX命令,也可以用Redisson提供的锁。
缓存雪崩则是大量key在同一个时间点集体过期,或者Redis服务本身挂了,导致请求全部打到数据库。前者可以通过设置随机过期时间解决,比如基础过期时间300秒加上一个0到60秒的随机值;后者必须做高可用部署,也就是后面会说到的Redis主从加哨兵架构。
黑马商城的实战案例里都覆盖了这些场景,但很多人在学习时只是敲代码敲过去了,没有仔细想为什么这么写。比如互斥锁那一段代码,你仿照写出来容易,但如果我让你解释锁的时间设置成多少合适、线程等待期间要不要限流、拿到锁之后查询数据库又失败了要不要释放锁,很多人就答不上来了。这些问题才是工作中真正会碰到的。
3.3 基于Redis实现分布式锁
分布式锁是分布式系统里面老生常谈的话题。黑马商城项目中用到的场景主要是防止订单超卖和防止重复下单。如果只是把concurrent的Lock用在单机代码里,一到多实例部署就失效了,因为每个JVM内部的锁互相不可见。
解决思路就是让多个进程抢同一个Redis key,谁抢到谁执行。最简单的实现是用SETNX key value,如果返回1代表抢锁成功,返回0说明锁被其他人持有。但这里的坑非常多。比如拿到锁后进程崩溃了,如果没有设置过期时间,锁永远不会释放,其他线程就被卡死;如果设置了过期时间但业务执行时间超过了过期时间,锁提前释放,其他线程又进来了,导致并发安全问题。
所以生产环境里我不建议手写SETNX逻辑,直接用Redisson就好。Redisson提供的RLock底层会自动续期,默认每10秒检查一次,只要当前线程还在执行,就会把锁的过期时间延长到30秒,这个机制俗称“看门狗”。它同时提供了可重入、获取锁超时控制等功能,比手写可靠得多。
我做过一次压测,用Redisson锁住秒杀接口,客户端并发量跑到5000的时候,订单创建成功率和库存扣减一致性都保持得很好,响应时间也比较稳定,不会出现因为竞争锁导致的大量超时请求。如果你还停留在手写SETNX阶段,这篇文章看完后建议去把Redisson接入你的项目试试,你会爱上它的。
3.4 Session共享与登录状态管理
单体应用时代,用户登录后会话信息放在应用服务器的内存里,没人在乎换了一台服务器之后Session还在不在。但到了分布式架构,前端请求通过Nginx负载均衡,可能第一次请求落在服务器A,第二次落在服务器B,如果两台服务器的Session不互通,用户就会莫名其妙被弹出登录状态。
黑马商城项目通过Spring Session结合Redis来解决这个问题。引入spring-session-data-redis依赖后,原本的HttpSession会被一个Redis支持的实现所替代,Session数据自动写入Redis,所有服务节点共享同一份Session数据。
这里有一个我踩过的坑:Session数据在Redis中默认是用JDK序列化方式存储的,虽然功能没问题,但你在Redis里看到的是一堆二进制乱码,不直观。如果希望Session内容可读,可以配置一个自定义的RedisSerializer。同时要记得给Session设置合适的过期时间,Spring Session默认是30分钟,在电商场景中这个时间有点短,我习惯调整到2小时,避免用户逛着逛着购物车就断线了。
3.5 购物车与库存扣减的原子性处理
电商项目里最考验技术功底的就是库存扣减,超卖就是在这时候出现的。黑马商城的秒杀功能实现里用到了Redis的Lua脚本,这是个很关键的技术点。
为什么用Lua脚本?因为它能保证多个Redis命令在同一个脚本里执行的原子性。比如检查库存是否充足、扣减库存、创建订单这三步,如果你分三次发Redis命令,中间任何一次网络抖动都会导致状态不一致,甚至出现扣了库存订单却没生成的情况。把它们写进一个Lua脚本里,Redis会整体执行,执行期间不会插入其他命令。
同时,为了防止一人一单,还会把用户ID作为锁的维度,结合Redis的SetNX实现接口级别的防重。这里又涉及到分布式锁的一个变体:锁的粒度。拿秒杀场景来说,如果一个用户一次只能秒杀一单,那么锁的维度应该是用户ID;如果商品的总库存是多件,那么扣减时的操作对象就应该是库存key的原子递减。这两种用法一组合,基本能把超卖和重复下单都杜绝掉。
我在带队开发商城项目时,把这一套秒杀方案原封不动搬到了真实项目里,聚合了5000路并发压测,最终结果是库存误差为0,订单数据全部有效。建议你在学完这个方法后,自己也写一个Jmeter测试脚本模拟并发下单,亲眼看看在没有Lua脚本和锁保护的情况下库存是怎么变负的,这种直观对比会让你印象深刻得多。
4. 从开发到部署的完整流水线
4.1 构建打包与多环境配置管理
开发阶段环境、测试环境、生产环境的配置经常不一样。如果你直接把本机数据库地址、Redis密码等硬编码在application.yml里,部署到线上肯定跑不通。黑马商城这种项目通常的做法是使用多Profile配置:application-dev.yml、application-prod.yml,启动时通过spring.profiles.active来指定用哪套配置。
打包命令我一般用mvn clean package -Dmaven.test.skip=true,跳过测试直接打jar包。这里有个细节是不要把src/main/resources下的任何文件改动直接覆盖到包外目录,因为Spring Boot的jar包内部有自己的classpath目录。
我之前遇到过一个问题:部署时为了修改Redis地址,直接进到jar包里改了application.yml,再把jar包扔到服务器上运行,结果怎么都不生效。后来才意识到Spring Boot加载配置的优先级是:外部配置文件高于jar包内部配置。正确做法是把application.yml放在jar包同级的config目录下,Spring会自动读取外部配置并覆盖内部的值。这个优先级规则如果你没搞明白,会浪费很长的时间在“明明改了配置却不生效”的坑里。
4.2 Docker部署Redis并搭建主从集群
上线环境里单节点Redis风险很大,万一Redis进程挂了,缓存直接不可用,所有流量瞬间砸向数据库。所以生产部署至少要上主从架构。黑马商城的部署篇里用的就是Docker Compose方式。
第一步先创建主节点的配置文件redis-master.conf,开启持久化:appendonly yes,设置密码:requirepass 123456。然后建两个从节点配置文件redis-slave1.conf、redis-slave2.conf,在配置文件里指定replicaof master-ip 6379和masterauth 123456。如果你的Docker网络里主机名是master,直接写replicaof master 6379即可。
用docker-compose.yml把三个服务编排起来,执行docker compose up -d启动,然后进入从节点容器执行info replication,看到role:slave且master_link_status:up,就说明主从同步成功了。这里最关键的一点是,主节点必须开启持久化,否则一旦主节点重启,它的数据是空的,从节点会跟着把自己清空重新同步,造成数据丢失。
我见过一些团队部署主从时只关注同步状态,忽略了主节点持久化,结果某天Redis宕机重启后,所有缓存数据归零,数据库直接被流量打穿。这种事故是可以提前规避的,主节点开appendonly的成本不过一点点磁盘空间,干嘛不给自己留条活路呢。
4.3 部署Spring Boot应用至服务器
Spring Boot应用打包成jar包后,部署就简单了。你可以直接执行java -jar black-mall.jar启动,但直接这样跑有很多隐患。首要问题是关闭终端窗口时进程会跟着退出,其次日志不好管理,而且进程崩溃后不会自动重启。
更职业的做法是使用systemd管理进程。写一个black-mall.service文件放到/etc/systemd/system目录下,定义ExecStart为java -jar /opt/black-mall/black-mall.jar,再定义StandardOutput和StandardError把日志写到指定文件。配置好后systemctl daemon-reload,再systemctl start black-mall就能实现开机自启和挂了自动重启。
当然,如果你追求更标准的容器化部署,给应用写一个Dockerfile然后镜像运行也很干净。基础镜像用openjdk:8-jdk-alpine,把jar包复制进去,暴露8080端口,启动命令是java -jar。然后通过docker compose和Redis、MySQL编排在一起,一条命令把整套环境拉起来。我团队现在新项目都走这套方案,省心程度比systemd那种方式高不少。
4.4 Nginx反向代理与动静分离
应用部署好了,接下来要加一层Nginx来做反向代理。这台Nginx放在应用服务器前面,用户请求先进Nginx,再有Nginx转发到后端的Spring Boot服务。它能做的远不止转发,负载均衡策略、静态资源缓存、请求头过滤都可以在这里配置。
黑马商城前端页面里有大量的图片、CSS、JS文件,如果这些静态资源也走Java应用去读取,会白白消耗Java线程。最理想的做法是在Nginx里配置一个location /static/,直接映射到服务器的静态文件目录,让Nginx直接返回文件,根本不经过Java进程。同时可以开gzip压缩大幅减少流量传输体积,响应速度提升非常明显。
反向代理配置里还要注意proxy_set_header的设置。如果不设置X-Forwarded-For和Host,后端的Java应用获取不到用户的真实IP,导致日志里全是Nginx的IP,到时候想排查用户来源就抓瞎了。我建议在location块的proxy_set_header里把Host、X-Real-IP、X-Forwarded-For都配齐,保证链路信息完整。
4.5 监控与日志收集
部署上线只是开始,怎么保证运行稳定才是真正的考验。Redis本身提供了非常实用的监控命令,redis-cli进入交互模式后执行info,能直观看到内存使用量、客户端连接数、命中率、持久化状态等关键指标。日常巡检时我会特别关注hit_rate命中率,如果低于80%,就要反思缓存的有效性是否衰退了,是不是大量key没被命中导致请求穿透到数据库。
slowlog get是排查Redis变慢的利器。它会把执行时间超过指定阈值的命令记录下来。默认阈值是10000微秒,也就是10毫秒。线上我会调到2000微秒,把超过2毫秒的操作都记录下来。如果发现某个请求频繁出现在慢日志里,就要考虑优化缓存结构或者减少大key的读取。
应用侧的监控可以用Spring Boot Actuator,暴露/actuator/health、/actuator/metrics端点,配合Prometheus抓取指标,再用Grafana做可视化大屏。这一整套下来,Redis的CPU、内存、命中率、连接数全部图形化呈现,预警规则配置好之后,半夜收到告警短信的概率都会低很多。
5. 实战中的常见问题与排查技巧
5.1 缓存与数据库的数据不一致
这是Redis缓存场景里最经典的问题。根本原因是双写时的并发竞争。举个例子:A请求更新了数据库,想把旧缓存删掉;B请求在你删除之前读到了旧缓存,直接返回了旧数据。如果业务不能容忍这种时延,就需要额外手段来兜底。
我做电商缓存这么多年,总结下来最实用的思路是结合业务场景选择方案。对于价格、库存这类一致性要求高的数据,处理方式是:更新数据库后直接删除缓存,同时开启延迟双删策略,也就是主线程删除一次缓存后,再通过一个延迟任务在500毫秒后删第二次。对于商品名称、图片这些允许短暂不一致的数据,直接把过期时间设置得短一些,比如5到10分钟,即使出现问题也会很快自愈。
还有个进阶操作是监听MySQL的binlog,通过Canal中间件把数据库变更同步给Redis做更新,这就是生产环境的最终一致性方案了。黑马商城项目本身没有引入这么重的组件,但你如果去面试大厂,提到这个方案绝对会让面试官眼前一亮。
5.2 Redis连接超时与连接池耗尽
很多人在本地开发一切正常,一部署到Linux服务器就频繁报出Redis command timed out或者RedisConnectionException,尤其是使用Lettuce客户端时更容易遇到。这类问题大概率不是Redis真的挂了,而是网络配置和连接池参数的问题。
Lettuce是Spring Boot 2默认的Redis客户端,它基于Netty实现连接复用。在高并发下如果你没有合理配置连接池的上限,大量的线程会同时等待获取空闲连接,一旦池子被占满,请求就会排队等锁,出现超时。我常用的一组参数是maxTotal=50、maxIdle=30、minIdle=10,你可以根据自己的并发量调整。
还有一个经常被忽略的地方:Redis的timeout参数。如果你在Redis配置文件里把timeout设置成了一个很小的值,当连接空闲时间超过这个值,Redis服务端会主动断开连接,而客户端的连接池不知道连接已经失效,还在尝试复用,就会抛出连接异常。解决思路是Redis服务端的timeout不要设置得太小,同时客户端增加validateConnection或者用带健康检查的连接池。
5.3 Redis序列化乱码与线上问题
这是新手最容易踩的坑。使用Spring Data Redis时,默认的RedisTemplate采用的序列化器是JdkSerializationRedisSerializer,当你往Redis里存一个对象,比如Store类的实例,控制台里看到的将是类似xACxEDx00x05t...这种乱码。功能上虽然能正常读写,但一旦你想跨语言或其他工具去查看数据,就会非常痛苦。
规范做法是通过自定义RedisTemplate配置来替换序列化器。Key使用StringRedisSerializer,Value使用GenericJackson2JsonRedisSerializer。这样存进去的数据是可读的JSON,排查问题比较方便。但注意,使用JSON序列化意味着对象里必须有无参构造函数,否则反序列化会直接报错。黑马商城教程里的DTO类都满足这个条件,但你自己扩展功能时,千万记得加上无参构造。
另一个坑是字符串类型的缓存。如果业务里用StringRedisTemplate存了数据,但读取时用RedisTemplate读,就会出现类型转换异常。原因还是两者使用的序列化器不同。所以我会严格要求团队:所有字符串类型的操作统一走StringRedisTemplate,所有对象类型的操作统一走自定义的RedisTemplate,不混用,能省掉无数个“为什么读不到数据”的排查时间。
5.4 分布式锁失效的致命场景
分布式锁并不是加了就一劳永逸的。我总结过三个典型的失效场景,每个都在实际项目中出现过,必须了解清楚。
第一个场景是锁的过期时间小于业务执行时间。假如业务逻辑要跑10秒,但锁的过期时间只设置了5秒,锁自动释放后另一个线程进来了,就出现了并发安全问题。解决代码里用的是Redisson,它的看门狗机制就是为了应对这种情况。
第二个场景是主从切换时锁丢失。客户端A在主节点上拿到了锁,但主节点还没把这条数据同步到从节点时就挂了,哨兵把从节点提升为主节点后,从节点上没有这条锁记录,客户端B就也能拿到同一把锁了。这个问题在严格一致性要求的场景下需要用Redlock红锁方案,但它在性能和可用性上有一定取舍,不是所有业务都需要这么重的方案。
第三个场景是GC停顿时间过长。JVM发生Full GC导致线程长时间停顿,锁过期了而线程还在执行,等到GC恢复后业务还在跑,但锁实际已经易主了。这个问题连Redisson的看门狗也无法完全避免,因为看门狗线程也可能因为GC冻结。好在这种场景概率较低,如果真的不能容忍,可以在业务里加上版本号或者使用数据库的唯一约束做兜底。
5.5 大Key与热Key治理
当Redis的某个key存的数据特别大,或者某个key被超高频率地访问,就会成为大Key或者热Key。大Key的危害是阻塞网络,因为通过网络传输一个10MB的string,一次传输就会占用很长时间,影响同Redis实例上的其他请求;热Key则会让单个Redis节点CPU飙升,拖垮整体性能。
排查大Key可以使用redis-cli --bigkeys命令,它会遍历所有的key并按类型统计大小。对于String类型的一般看value字节数,对于Hash和ZSet类型则看元素数量。如果发现大Key,解决办法是拆分。比如一个用户购物车列表,如果全部塞进一个key里,数据量大了就拆成每个商品一个key,然后通过管道批量读取。热Key的解决办法则是做本地缓存,让热点请求在应用内存层直接返回,不再打到Redis上,或者给热Key加上随机的后缀,分散到多个节点上。
在压测黑马商城那些秒杀接口时,你会发现秒杀商品的库存key就是典型的瞬时热Key,所有请求都在抢这同一个key。我当时的处理方式是加了一层本地缓存把库存快速扣减,然后用异步批量同步到Redis,这个思路其实已经偏向CQRS模式了,效果非常明显。
5.6 内存淘汰策略与碎片优化
生产环境的Redis内存是有限的,当内存满了之后Redis会按照配置的淘汰策略去清除一部分key。如果你不设置maxmemory和maxmemory-policy,默认情况是64位系统上不限制内存使用,数据一直往里面塞,直到Linux物理内存耗尽,然后OOM killer把Redis进程干掉,这就真的尴尬了。
为此我需要主动设置内存上限,比如maxmemory 2gb,并选择allkeys-lru淘汰策略,让最久没被访问的key先被淘汰。电商项目里缓存的数据大多是热点查询,很适合用LRU来淘汰冷数据。
内存碎片是另一个容易踩的坑。Redis里的对象经常创建和删除,内存空闲列表会变得碎小的比较多,导致实际内存占用很高。你观察info memory里的mem_fragmentation_ratio指标,如果持续在1.5以上,说明内存碎片严重,此时执行redis-cli memory purge命令可以尝试回收一些碎片空间。对于持久化开启的实例,还有一种方式是重启Redis,重启会重新加载RDB或AOF文件,通常能有效降低碎片率。
做完整套黑马商城的部署之后,我强烈建议你养成定期巡检Redis内存状态的习惯。一条info memory命令,几十个指标,花两分钟时间看一眼,就能提前发现很多隐患,别等线上出事之后才想起去诊断。
说回这套教程本身,我觉得它在国内Java项目实战中是个很好的标杆。你跟着流程走一遍,收获的绝不仅仅是能写几个缓存注解和操作API,更重要的是理解了高并发场景下数据一致性和系统稳定性的保障思路。如果你后续真的去做高并发电商项目,你会发现这里的很多设计理念是完全可以落地的,这也正是它被大量培训机构和企业拿来当教材的原因。