1. 项目概述与整体设计思路1.1 为什么选Iris Redis这个组合这两年做Go后端Iris Redis几乎成了我接手过的所有高并发业务线的标配组合。Iris负责把HTTP请求接进来、把路由和中间件管好Redis负责扛住那些高频率、低延迟的重复查询两者配合起来非常顺。先说Iris。很多人纠结Gin和Iris到底选哪个我的感受是Gin胜在轻量和生态稳定Iris则在MVC支持、依赖注入、Websocket集成、国际化这些工程化能力上更完整。如果你要做的不是一个简单的API Demo而是一个需要长期维护、多人协作、功能边界清晰的业务系统Iris的工程项目骨架能帮你省掉不少组织代码的时间。尤其是它的iris.mvc包一套Controller写下来路由注册、参数绑定、响应处理都在一个类里业务代码结构非常清晰。再说Redis。后端只要碰到“热点数据”、“会话状态”、“计数器”、“排行榜”、“分布式锁”、“消息暂存”这些场景几乎绕不开Redis。它把数据放在内存里读写都是微秒级单实例QPS轻松过10万而且支持String、Hash、List、Set、ZSet五种基本结构加上HyperLogLog、Geo、Stream这些高级结构基本能覆盖业务里80%以上的实时数据诉求。这个组合最典型的落地方式是Iris这层负责协议解析、参数校验、业务编排Redis这层负责把数据库查询结果、登录态、热点数据、临时状态全部缓存起来让MySQL只处理真正需要落盘的写操作和低频查询。我这个实战项目参考的是资讯类App的后端模型核心链路包括用户登录、文章列表、文章详情、阅读排行、点赞去重。麻雀虽小但该踩的坑基本都踩了一遍接下来我把设计过程和完整实现拆开讲。1.2 项目整体架构与核心模块拆分这个项目我拆成了五个基础模块分别是路由与中间件层Iris负责HTTP入口统一处理CORS、日志、恢复、JWT鉴权。缓存管理层封装Redis客户端统一处理连接池、序列化、过期时间、空值缓存。业务服务层用户、文章、排行榜、点赞、会话每个业务域独立成Service。数据结构层根据业务特点选择合适的Redis结构不盲目用String。部署运维层Docker Compose跑Redis主从配合界面工具做可视化排查。这样拆分的好处是每个模块的职责单一Redis的Key前缀、过期策略、序列化方式都集中在缓存管理模块里换缓存组件或者调整连接池参数时不用满项目找代码。后面我会按照这个架构一步步把代码写出来。2. 环境准备与基础工程搭建2.1 安装Go环境与项目初始化这个项目我用的是Go 1.20以上的版本Iris框架现在要求Go模块开启安装很简单直接去官网下载对应系统安装包或者用包管理器一键装。装完以后验证一下go version能输出版本号就说明环境OK。然后创建项目目录mkdir iris-redis-demo cd iris-redis-demo go mod init iris-redis-demo导入Iris框架go get github.com/kataras/iris/v12latestRedis客户端我用的是go-redis它是目前Go社区里使用最广的Redis客户端支持连接池、哨兵、集群模式API设计也比较顺手go get github.com/redis/go-redis/v92.2 初始化Iris应用与基础中间件创建一个main.go先把Iris应用骨架搭起来。这里有个小细节初期就挂上中间件后面加路由时才能保证所有请求都会经过统一处理不然后补中间件容易漏掉接口。package main import ( github.com/kataras/iris/v12 github.com/kataras/iris/v12/middleware/logger github.com/kataras/iris/v12/middleware/recover ) func main() { app : iris.New() // 全局中间件恢复panic 访问日志 app.Use(recover.New()) app.Use(logger.New()) // 路由注册 app.Get(/health, func(ctx iris.Context) { ctx.JSON(iris.Map{status: ok}) }) app.Listen(:8080) }recover.New()的作用是捕获业务代码里的panic避免某个请求出错直接把整个进程带崩生产环境里必须有。logger.New()会记录每个HTTP请求的方法、路径、状态码、耗时后面排查问题很有用。启动项目go run main.go浏览器访问http://localhost:8080/health能看到{status:ok}说明Iris基础骨架跑通了。2.3 接入Redis客户端与连接池配置接着把Redis客户端实例化到项目里。我习惯单独建一个cache/redis.go文件集中管理连接配置package cache import ( context time github.com/redis/go-redis/v9 ) var Rdb *redis.Client func InitRedis() { Rdb redis.NewClient(redis.Options{ Addr: 127.0.0.1:6379, Password: , DB: 0, PoolSize: 100, // 连接池最大连接数 MinIdleConns: 20, // 保底空闲连接 DialTimeout: 5 * time.Second, // 连接超时 ReadTimeout: 3 * time.Second, // 读超时 WriteTimeout: 3 * time.Second, // 写超时 }) // 启动时检查连通性 ctx, cancel : context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err : Rdb.Ping(ctx).Err(); err ! nil { panic(Redis connection failed: err.Error()) } }连接池这几个参数必须解释一下因为我见过太多人在这里翻车。PoolSize代表客户端最多同时维持多少个TCP连接到Redis。设太大会让Redis服务器频繁切换上下文设太小并发一高就会出现等待连接的超时。我个人经验是单实例Redis普通业务机开到100到200就够用了除非你的Redis服务器本身配置非常强。MinIdleConns的作用是保持一定数量的空闲连接避免突发流量时现去建连。TCP建连虽然快但高并发下几十毫秒的建连时间也够让接口超时了这个参数在流量波动大的场景下特别有用。然后是ReadTimeout和WriteTimeout这两个建议不要设太大3秒足够。Redis操作通常是微秒级如果3秒还没返回说明Redis卡死了继续等下去反而让请求堆成雪崩。改一下main.go初始化Redispackage main import ( github.com/kataras/iris/v12 github.com/kataras/iris/v12/middleware/logger github.com/kataras/iris/v12/middleware/recover iris-redis-demo/cache ) func main() { app : iris.New() app.Use(recover.New()) app.Use(logger.New()) // 初始化Redis cache.InitRedis() app.Get(/health, func(ctx iris.Context) { ctx.JSON(iris.Map{status: ok}) }) app.Listen(:8080) }如果没有装Redis现在去装一个。Windows用户直接到官方仓库下载zip包解压运行Linux用户用apt install redis-server或者yum install redis都行。验证Redis可用redis-cli ping返回PONG就说明Redis服务正常。3. 核心业务场景的Redis落地实践3.1 String结构实现热点数据缓存项目里最核心的接口是文章详情。文章内容在MySQL里但高并发用户都会集中在最新几篇文章上每次都查数据库完全没必要。我用String结构做文章详情缓存Key设计为article:detail:{id}Value存JSON序列化后的文章对象过期时间设为30分钟。业务逻辑是这样的func GetArticleDetail(ctx iris.Context) { articleID : ctx.Params().GetIntDefault(id, 0) if articleID 0 { ctx.StatusCode(iris.StatusBadRequest) ctx.JSON(iris.Map{msg: invalid article id}) return } // 1. 先查缓存 cacheKey : fmt.Sprintf(article:detail:%d, articleID) val, err : cache.Rdb.Get(ctx, cacheKey).Result() if err nil { // 缓存命中直接返回 var article Article json.Unmarshal([]byte(val), article) ctx.JSON(article) return } // 2. 缓存未命中查数据库 article, err : queryArticleFromDB(articleID) if err ! nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.JSON(iris.Map{msg: article not found}) return } // 3. 回填缓存设置过期时间 data, _ : json.Marshal(article) cache.Rdb.Set(ctx, cacheKey, data, 30*time.Minute) ctx.JSON(article) }这个“先查缓存、没命中就查库、查完回填缓存”的流程专业叫法是Cache Aside Pattern是缓存和数据库组合使用最经典的方案。但直接这样写有一个隐患如果某个文章ID在数据库里根本不存在每次请求都会穿透到数据库缓存一直不命中数据库压力反而更大。解决办法是把空值也缓存起来设置一个很短的过期时间比如5分钟// 查库没找到时缓存空值防止缓存穿透 cache.Rdb.Set(ctx, cacheKey, , 5*time.Minute) ctx.StatusCode(iris.StatusNotFound) ctx.JSON(iris.Map{msg: article not found})这里我建议约定一个规则空缓存用空字符串或null字符串占位查询命中时先判断是不是空占位是就直接返回不存在。3.2 Hash结构存储用户信息与Session用户登录后需要频繁读取用户资料比如昵称、头像、等级、手机号这些字段如果每次都查MySQL开销不小。而且用户信息经常是部分字段更新如果用String整体序列化更新一个字段就要重写整个对象。Hash结构正好适合这种场景它可以按field维度的读写。我把用户信息缓存在user:info:{id}里// 登录成功后回填用户信息缓存 cache.Rdb.HSet(ctx, fmt.Sprintf(user:info:%d, userID), map[string]interface{}{ nickname: user.Nickname, avatar: user.Avatar, level: user.Level, }) cache.Rdb.Expire(ctx, fmt.Sprintf(user:info:%d, userID), 2*time.Hour)读取时按需取字段nickname, err : cache.Rdb.HGet(ctx, fmt.Sprintf(user:info:%d, userID), nickname).Result()这样修改昵称时也只需要cache.Rdb.HSet(ctx, fmt.Sprintf(user:info:%d, userID), nickname, newNickname)不需要把整个用户对象读出来再重写一遍。Hash结构在这个场景下的优势非常明显。Session管理我同样是基于Redis实现的。Iris自带的sessions管理器支持Redis后端存储配置起来很简洁在main.go里加上import ( github.com/kataras/iris/v12/sessions github.com/kataras/iris/v12/sessions/sessiondb/redis ) // Redis session数据库连接 db : redis.New(redis.Config{ Network: tcp, Addr: 127.0.0.1:6379, MaxAge: 24 * time.Hour, // session过期时间 Database: 1, // 使用DB1隔离业务缓存和session }) sess : sessions.New(sessions.Config{ Cookie: iris_session_id, Expires: 24 * time.Hour, Storage: db, })Session放在Redis里最大的好处是多个Iris实例可以共享同一份Session数据方便横向扩容。之前用单机内存Session部署两台机器时用户就会被随机踢下线换成Redis存储Session后这个问题彻底消失。3.3 用Set结构实现点赞去重文章点赞功能最核心的需求是“一个人只能给同一篇文章点一次赞”。如果靠MySQL查记录再判断每次点赞都要一次事务操作数据库压力很大。用Redis的Set结构做一个去重集合性能会好得多。以article:liked:{id}为Key这个Set里存所有点过赞的用户ID每次点赞前先判断用户是否已经在集里func LikeArticle(ctx iris.Context) { userID : ctx.Values().GetIntDefault(user_id, 0) articleID : ctx.Params().GetIntDefault(id, 0) likedKey : fmt.Sprintf(article:liked:%d, articleID) // SADD返回1表示添加成功返回0表示已存在 added, err : cache.Rdb.SAdd(ctx, likedKey, userID).Result() if err ! nil { ctx.StatusCode(iris.StatusInternalServerError) ctx.JSON(iris.Map{msg: like failed}) return } if added 0 { ctx.JSON(iris.Map{msg: already liked}) return } // 用INCR同步累加点赞总数 likeCountKey : fmt.Sprintf(article:like_count:%d, articleID) cache.Rdb.Incr(ctx, likeCountKey) ctx.JSON(iris.Map{msg: like success}) }这里用SAdd的返回值来判断是否已经点过赞一步操作完成“判断 写入”不需要先查再写。点赞总数用INCR命令Redis内部保证原子性高并发下不会出现计数误差。还支持用一个SUnionStore命令把多篇文章的点赞用户做并集比如拉取“我赞过的所有文章”直接对多篇文章的Set做并集性能也非常可观。3.4 ZSet结构实现实时排行榜资讯类项目经常要做“阅读榜Top10”这种需求如果用SQL做ORDER BY read_count DESC LIMIT 10数据量一大就吃不消。ZSet是Redis里有意思的一个结构它内部维护了一个按score排序的跳表天然适合做排行榜。每篇文章每产生一次阅读就更新它的阅读数ZSet的score就是阅读数func RecordRead(ctx iris.Context) { articleID : ctx.Params().GetIntDefault(id, 0) // 文章阅读排行榜score为阅读次数 rankKey : ranking:article_read cache.Rdb.ZIncrBy(ctx, rankKey, 1, fmt.Sprintf(%d, articleID)) }查询Top10排行榜func GetArticleRank(ctx iris.Context) { rankKey : ranking:article_read // ZRevRange按score从高到低取出前10名 res : cache.Rdb.ZRevRangeWithScores(ctx, rankKey, 0, 9).Val() var list []map[string]interface{} for _, z : range res { list append(list, map[string]interface{}{ article_id: z.Member, read_count: z.Score, }) } ctx.JSON(list) }ZSet底层是“字典 跳表”双结构按member找score是O(1)按score范围查排名是O(logN)性能相当稳。排行场景只要数据量在千万级以内Redis的ZSet都能扛得住。还有一个小巧的思路如果想看“每小时热榜”可以按小时作为Key的一部分比如ranking:article_read:2025012114定时清理旧Key就行。3.5 基于Lua脚本实现分布式锁多实例部署时用户同时对一个资源做写操作比如“同一用户30秒只能发一次评论”单机内存锁没法跨实例生效这就需要分布式锁。分布式锁的实现要点有四个互斥性只有一个客户端能拿到锁。安全性锁要有过期时间防止持有锁的机器宕机造成死锁。原子性加锁和解锁操作不能被打断。防误删只能删除自己持有的锁。用Redis实现分布式锁加锁用的是SET NX EX一步完成“不存在才设置”和“设置过期时间”。Java里常见的Redisson框架还支持看门狗续期Go里我们用Lua脚本实现一个简版// 加锁 func AcquireLock(ctx context.Context, key string, requestID string, ttl time.Duration) bool { // NX表示key不存在时才设置EX表示设置过期时间 ok, err : cache.Rdb.SetNX(ctx, key, requestID, ttl).Result() if err ! nil { return false } return ok } // 解锁只有value匹配才删除 var unlockScript redis.NewScript( if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end ) func ReleaseLock(ctx context.Context, key string, requestID string) { unlockScript.Run(ctx, cache.Rdb, []string{key}, requestID) }加锁用SetNX一条命令做完检查和设置保证了原子性。解锁用Lua脚本先比对value再删除确保不会误删别人刚获取的锁。这里requestID我会用UUID或者用户ID 时间戳拼接确保每个请求唯一。至于“看门狗续期”在生产环境里是一个经常被讨论的话题。如果锁的TTL设得太短业务还没执行完锁就自动过期了另一个请求会拿到锁并发问题又回来了。如果TTL设得太长持有锁的机器崩了其他请求要等很久才能拿锁。我在这个项目里采用的是“TTL 30秒 业务逻辑结束后主动释放”并且把业务操作的耗时严格控制在锁过期时间以内。3.6 用List结构实现异步任务队列Iris处理用户注册成功后需要发欢迎邮件、推送优惠券、记录用户事件日志。这些操作如果都同步执行接口耗时会被拖到几百毫秒。我用List结构做简单的任务队列// 把任务塞入队列 task : map[string]interface{}{ type: send_welcome_email, user_id: userID, to: user.Email, } data, _ : json.Marshal(task) cache.Rdb.LPush(ctx, queue:async_task, data)后台起一个goroutine循环消费go func() { for { data, err : cache.Rdb.BRPop(ctx, 5*time.Second, queue:async_task).Result() if err ! nil { continue } handleTask(data[1]) } }()LPushBRPop的组合是一个典型的“生产者-消费者”模式。BRPop在队列为空时会阻塞等待不会空转CPU。任务处理失败时可以重新LPush回去做重试。这里我多说一句为什么不用Redis直接替代专业的消息队列。Redis List做任务队列只适合任务量不大、不需要精确投递语义、不需要消息回溯的场景。到了消息量很大、需要消费者分组、需要延迟队列的时候还是老老实实上RabbitMQ或者Kafka。我在项目里用Redis List只是因为注册邮件的任务延迟几分钟无所谓没必要为了这个场景引入一套新的中间件。4. 生产环境部署与运维实践4.1 用Docker Compose部署Redis主从本地开发用单机Redis足够一旦上生产环境至少要做主从架构Redis主节点负责写和读从节点负责读并且作为冷备数据存在。我分享一下项目里用的Docker Compose配置version: 3.8 services: redis-master: image: redis:7.0-alpine container_name: redis-master ports: - 6379:6379 command: [redis-server, --appendonly, yes, --requirepass, yourpassword] volumes: - redis-master-data:/data networks: - redis-net redis-slave: image: redis:7.0-alpine container_name: redis-slave ports: - 6380:6379 command: [redis-server, --slaveof, redis-master, 6379, --masterauth, yourpassword, --requirepass, yourpassword] depends_on: - redis-master volumes: - redis-slave-data:/data networks: - redis-net volumes: redis-master-data: redis-slave-data: networks: redis-net:使用Docker部署Redis有一个好处是版本统一不需要每台机器单独装Redis。我在生产环境踩过一个坑某台机器的Redis还停留在5.x另一个环境的Redis已经是7.x一些业务代码在5.x上跑得好好的到了7.x上却因为客户端版本太老出现协议不兼容的问题。用Docker镜像统一版本之后这类问题就很少出现了。启动服务docker compose up -d用docker-compose ps查看状态两个Redis容器都在运行主从就部署好了。验证主从同步# 在主节点写入 redis-cli -a yourpassword set testkey hello # 在从节点读取 redis-cli -p 6380 -a yourpassword get testkey能读到hello主从复制正常。4.2 Redis可视化工具与日常运维命令行工具redis-cli虽然功能强大但在排查复杂数据结构时效率太低。我日常用的是Another Redis Desktop Manager跨平台支持Windows、macOS、Linux免费开源连接配置也很友好。连接Redis之后能看到每个DB下的Key列表还支持按Key前缀搜索、直接查看String、Hash、ZSet里具体数据排查缓存数据问题时非常方便。有一点要注意生产环境的Redis配置了密码连接时要把密码填上另外尽量用SSH隧道连接不要直接把Redis端口暴露在公网上。日常运维我还会定期检查三个核心指标都通过redis-cli就能看# 查看内存占用 redis-cli info memory # 查看连接数 redis-cli info clients # 查看慢查询 redis-cli slowlog get 20其中慢查询这个指标生产环境里非常重要。Redis默认执行时间超过10毫秒的命令会被记录到慢查询日志里如果发现大量慢日志注意力要立刻转向那些大Key和热Key的问题。曾经有一次我排查接口卡顿最后发现是一个HGETALL拉取了包含几十万字段的Hash每条命令执行了将近200毫秒把Redis拖得很慢。后面规范了数据结构设计把大Hash拆成了多个小Hash慢查询就消失了。4.3 序列化方案选择与注意事项Redis存储的Value本质上都是字节流但你在代码里看到的是各种结构化的对象这就牵扯到序列化。我在项目里统一使用JSON作为序列化方案。JSON的好处是跨语言兼容、调试方便另一个重要作用是可以让Value在Redis Desktop Manager这类可视化工具里直接查看排查问题很直观。不过JSON序列化有一个必须注意的点不要使用Go结构体的默认序列化字段名。比如ArticleID默认会序列化成ArticleID可读性差而且还费内存。我一般在结构体上显式加标签type Article struct { ID int json:id Title string json:title Content string json:content AuthorID int json:author_id PublishAt time.Time json:publish_at }这样序列化出来的JSON体积小而且干脆利落。还有一点是关于数字类型的序列化。Go里面int64序列化成JSON是数字但前端JavaScript处理超大整数时会有精度丢失的问题。如果ID字段用的是雪花算法生成的int64建议序列化成字符串type Article struct { ID string json:id }我做过一次教训深刻的迭代文章ID用雪花算法生成后前端用Number类型接收导致部分ID出现精度丢失文章详情页跳转到了错误的文章。后来统一在序列化层把ID转成字符串才解决。4.4 持久化策略与重启恢复Redis虽然叫缓存但生产环境下很多数据是“不能丢”的比如用户Session、分布式锁的状态、排行榜数据。Redis的持久化方式有两种RDB快照和AOF追加日志。RDB是周期性把内存数据打快照存盘优点是恢复快、文件紧凑缺点是有丢失窗口两次快照之间的数据会丢。AOF是把每次写命令追加到日志文件可配置同步策略默认everysec表示每秒刷一次盘最多丢一秒数据。我对项目的要求是“Session丢几秒可以接受但不想故意丢”所以用AOF everysec。Docker部署时启动参数加--appendonly yes即可开启AOF。我见到过有团队为了省内存把AOF关了结果Redis一崩就丢了大半个Session池用户集体被踢下线这种事故说实话完全可以避免。另外提一个经验Redis重启恢复时如果AOF文件很大恢复过程会非常慢期间Redis无法提供服务。这时候可以考虑在流量低谷期做一次BGREWRITEAOF重写把AOF日志压缩瘦身让重启恢复速度更快。5. 常见问题与排查技巧实录5.1 Redis连接池耗尽导致接口大面积超时这个坑我踩得很典型。项目刚上线时用户量暴增Redis连接池参数用的是默认的PoolSize: 10 * GOMAXPROCS结果并发一上来大量请求排队等连接接口耗时直接飙到几秒。排查过程是这样的redis-cli info clients看到connected_clients一直顶在最大值。应用日志里出现了大量connection pool timeout或者dial tcp i/o timeout。用redis-cli --latency测了一下Redis本身延迟发现毫秒级说明Redis实例本身没有压力。问题定位到客户端连接池配置上每个请求都占用一个连接连接池太小不够分请求只能在池外排队。解决方案是把连接池调大并加上最小空闲连接保底同时把业务代码里无效的重复获取连接的操作优化掉。这个案例说明一个问题排查接口变慢不要第一时间怀疑Redis服务器性能先确认连接池和是不是有热点Key。5.2 缓存雪崩与缓存击穿的处理缓存雪崩简单说就是大量缓存同时过期导致请求全部落到数据库。我在这个项目里设过期时间时对同类业务加了随机偏移量// 文章详情缓存过期时间30分钟基础上加随机0-300秒 expire : 30*time.Minute time.Duration(rand.Intn(300))*time.Second cache.Rdb.Set(ctx, cacheKey, data, expire)加随机偏移量的目标是避免同一批Key在同一时刻集体过期。比如运营早上8点批量上架100篇文章如果全部设置完全相同的过期时间那么30分钟后它们又会在同一时刻集体失效DB压力会突然飙升。加了随机偏移量所有Key的过期时间错开对数据库的冲击就平缓了。缓存击穿则是指某个热点Key过期的瞬间大量请求同时穿透到数据库。因为热点文章可能同时被上万人读取刚好缓存过期就造成了一波流量打到MySQL上。处理方式有两种一是热点数据不过期后台起任务定时刷新二是用分布式锁只让一个请求去查DB回填缓存其他请求等待缓存回填后直接拿缓存。这个项目用的是第二种方案锁的粒度控制在单个Key级别不会影响其他Key的读写。5.3 缓存与数据库的数据不一致问题我遇到过的另一种典型场景是后台更新了文章标题但用户刷新页面后看到的还是旧标题过了一段时间才变好。这就是缓存和数据库之间的数据一致性问题。排查下来根因是更新数据库后没有主动删缓存一直依赖过期时间自然淘汰。解决方案是“先更新数据库再删除缓存”下次请求未命中缓存时自然会去数据库读最新数据并回填。这里有两件事要注意删除缓存失败要重试。如果更新完数据库后删除缓存失败缓存里就一直是旧数据。我通常把删除操作放到一个带重试机制的延时任务里失败了隔几秒再删。“延迟双删”。在高并发场景下先更新数据库再删缓存有可能在删除完成之前另一个请求正好把旧数据回填到缓存。所以我在更新数据库后先删一次缓存等几百毫秒再删一次把中间被回填的旧数据也清掉。我个人的经验是缓存一致性本来就是一个系统工程问题没有一劳永逸的方案。能接受短期不一致的业务就靠过期时间兜底要求严格的业务宁可多删几次缓存也别让旧数据长期留在缓存里。5.4 Redis内存碎片与Key淘汰策略Redis运行一段时间后内存碎片率会上升导致实际占用内存远超数据本身大小。我见过一个实例数据量只有2GBused_memory_rss却到了4GB。原因是大量Key写写删删内存页分裂严重。应对手段有两个方向一是检查业务代码里是否存在大量短生命周期Key比如原来设计了一个“临时验证码”每秒钟创建几万个但很快过期这种模式会加剧内存碎片二是用redis-cli memory purge主动清理或者直接重启实例让内存重新分配。还有一个必须提前设计的东西是Key淘汰策略。我在生产环境喜欢用allkeys-lru意思是在内存接近上限时优先淘汰最久没被访问的Key。这样即使缓存流量超出预期Redis也不会因为内存耗尽而拒绝写入而是自动腾出空间。在配置文件中加上maxmemory 4gb maxmemory-policy allkeys-lru这里有个坑如果业务里有绝对不能淘汰的数据比如分布式锁、正在使用的Session一定要给这类Key单独设置更长的过期时间或者在代码层面把它们放到单独的Redis实例里。否则LRU淘汰可能误伤关键数据导致分布式锁提前失效业务逻辑出现并发冲突。5.5 大Key与热Key的识别和处理最后聊一个生产环境最常见的“隐形杀手”大Key和热Key。所谓大Key指的是单个Key的Value特别大比如一个Hash里有几十万个字段或者一个String Value有几MB。这种Key的操作非常慢而且容易把网络带宽打满。排查方式redis-cli --bigkeys这个命令会扫描整个实例列出占用空间最大的Key。热Key则是某个Key在短时间内被大量请求集中访问。一个热门文章详情接口如果所有用户都只读同一个article:detail:10086这个Key每秒可能要承受几万次请求单分片CPU会跑满。处理热Key的常见手段是本地缓存把热点数据在应用内存里再缓存一层比如用Go的sync.Map做二级缓存本地命中就直接返回只有本地没有时才走Redis。我在文章详情场景里就做了二级缓存第一次查Redis后把数据同时放到进程内缓存有效期10秒。这样即使Redis热点Key压力再大最多也只有少量请求会穿透到Redis大部分都在本地内存直接返回了。这里提醒一句本地缓存引入后数据的实时性会变差。对于标题、文章内容这类允许几十秒延迟的业务完全没问题如果是库存、余额这种强一致数据就不要用本地缓存。写在最后的实践经验这个项目做完之后我最大的感受是Redis本身并不难难的是知道什么场景该用哪个数据结构以及如何在真高并发下不让缓存层变成新的瓶颈。Iris这个框架也一样用起来简单但路由怎么组织、中间件怎么挂、会话怎么存每一步选择都在影响日后的维护成本。我个人在实际项目里养成的一个习惯是把所有Redis操作的Key前缀做一个统一命名规范表比如article:detail:、user:info:、ranking:article_read存放在一个单独的文件里所有业务代码都引用常量而不是手写字符串。这样能避免不同业务之间Key冲突也方便在Redis Desktop Manager里按前缀搜索和清理缓存。另外Redis不是数据库。初始化项目时定一个原则Redis里只放可以接受丢失或可以重建的数据那些绝对不允许丢的数据一定得落到MySQL或者是其他存储里。认清这个边界后面能少踩很多坑。这个项目虽然不大但我把缓存穿透、击穿、雪崩、分布式锁、排行榜、任务队列这些常见问题都过了一遍整套代码跑下来算是一次相当完整的Redis实战训练。