我第一次用Redis,是在一个流量不大的后台系统里。当时的需求很简单——“把数据库查询结果缓存起来,少一点压力”,所以我的用法就是set + get,后来为了省事,所有key的过期时间都统一设成一小时。直到有一次活动页面流量上来,缓存大面积同时失效,数据库连接瞬间被打满,我盯着监控面板看错误率往上飙,才第一次意识到:Redis学习这件事,如果只停留在“会几个命令”的层面,基本等于在给未来的线上事故埋雷。
这篇文章把我从入门到踩坑的完整路线整理出来,覆盖安装下载、数据类型、底层机制、常见故障和项目落地。无论你是刚接触Redis的学生,还是工作中被迫接手Redis运维的开发者,都能在这里找到一条相对完整的参考路径。我不打算写成一堆命令的堆砌,而是想把“为什么这么设计”“为什么这个坑容易踩”也讲清楚。
1. 安装Redis之前,先把版本和下载方式想清楚
很多人一上来就apt install redis-server,装完能用就行。但Redis的版本差异其实挺大的,不同版本之间的特性、配置项、甚至部分命令行为都不一样。先把版本选明白,后面能少踩很多坑。
1.1 现在装哪个版本比较合适
Redis官方这几年的迭代明显加快。6.x引入了ACL权限控制、客户端缓存、I/O多线程;7.0把ziplist相关结构替换成了listpack,新增了Functions、Sharded Pub/Sub;7.2是相对成熟稳定的长支持版本;2025年发布的8.0则带来了多线程命令执行和内存与闪存混合存储,属于比较大的架构变化。
我的建议很简单:
- 生产环境的新项目,优先选7.2.x,稳定性和新特性平衡得最好。
- 老项目如果还在用6.x,短期不用焦虑,但尽量不要停留在5.x以下,5到7的差距已经很大了。
- 8.0可以装一套测试环境玩,别一上来直接上生产,刚发布的版本总会需要一点观察期。
推荐版本号的同时,要连带考虑发行方式。Linux上先用redis-server --version看一下现有版本,再决定怎么升级。
1.2 各平台的下载安装方式
官方源码包在download.redis.io这个地址下,各版本的tar.gz都有。Linux服务器最稳妥的方式是编译安装:
wget https://download.redis.io/releases/redis-7.2.5.tar.gz tar xzf redis-7.2.5.tar.gz cd redis-7.2.5 make -j4 make install PREFIX=/usr/local/redis编译完redis-server和redis-cli会被放到/usr/local/redis/bin下,建议把这两个二进制加到PATH里。make test也可以跑一遍,验证当前环境没问题。
Ubuntu/Debian上直接用apt装会方便很多:
apt update apt install redis-server systemctl enable redis-server systemctl start redis-servermacOS用户一条brew install redis就搞定,适合本机学习调试。Windows官方一直不支持原生安装,别再折腾什么Windows移植版了,直接用WSL2或者Docker跑干净利落:
docker run -d --name redis \ -p 6379:6379 \ -v /myredis/data:/data \ redis:7.2 \ redis-server --appendonly yes我第一次在Windows上踩过坑,装了个第三方编译的Redis,后来发现很多配置行为和官方版本不一致。从此学到一条经验:Redis相关的环境问题,先确认是不是安装来源不正规,而不是先怀疑自己的配置。
1.3 首次启动需要改的配置
用默认配置直接redis-server启动,很容易遇到两个问题:一是前台运行,ssh一断开服务就没了;二是protected-mode yes模式下的网络限制,让外部客户端连不上。
学习阶段我习惯改这几个配置项:
daemonize yes logfile "/usr/local/redis/redis.log" dir /usr/local/redis/data appendonly yesdaemonize yes让Redis以后台守护进程方式运行,logfile指定日志路径,排查问题全靠它。appendonly yes是开启AOF持久化,这个后面讲数据安全会细说,学习阶段建议直接开,不然redis一重启就发现数据全没了,心态容易崩。
启动之后用redis-cli验证一下:
redis-cli ping能看到PONG返回,说明服务正常了。再看一眼redis-cli info server里的版本号和运行模式,确认你启动的确实是那个编译好的Redis,而不是系统自带的旧版本。
2. 别再只当它是个缓存,Redis的本质是内存数据结构服务器
市面上讲Redis的文章,十个有八个开头都是“Redis是一个开源的高性能键值数据库”。这句话没错,但很容易让人产生误解,以为它就是个put/get的缓存工具。事实远不止如此。
2.1 从“键值存储”到“数据结构服务器”的认知转变
Redis和传统键值存储最大的区别在于:value不只是一个字符串,它可以是String、List、Hash、Set、ZSet、Bitmap、HyperLogLog、Geo、Stream等多种数据结构。这意味着很多业务逻辑可以直接在Redis里完成,而不必把数据拉到应用层处理。
举个例子,排行榜功能用ZSet实现,Redis内部已经把排序逻辑做好了,应用只需要ZADD往里加分数,查询时ZREVRANGE按分数倒序取前N名,十几行代码搞定。如果用MySQL实现,就是一个带ORDER BY score DESC LIMIT的查询,数据库压力一大就会成为性能瓶颈。
我在做社区功能的时候,很长一段时间把点赞数、评论数、热度分都放在MySQL里算,后来迁移到Redis的Hash和ZSet后,响应时间从几十毫秒降到了个位数毫秒。这种提升不是服务器变强了,而是数据结构和业务模型更匹配了。
2.2 单线程模型为什么还能这么快
这是个老生常谈但必须搞清楚的问题。Redis在处理命令时是单线程的,所有命令在一个主线程里依次执行,天然没有并发竞争和锁的消耗。那它为什么快?
首先,数据在内存里,内存访问本身是纳秒级的,比磁盘快几个数量级。其次,Redis的事件驱动模型基于I/O多路复用,通过epoll(Linux)/kqueue(macOS)在一个线程里去监听大量客户端连接,每次只在有socket事件真正发生时去处理,避免了阻塞等待。
这里有个大坑要提醒:单线程意味着任何一条命令执行过慢,后面所有命令都要排队等。所以KEYS *这种遍历全库的命令,在线上环境是绝对禁止的。我亲眼见过有人因为执行KEYS *导致Redis阻塞了几秒钟,那一瞬间整个业务的缓存全部超时。后面会专门讲慢查询和BigKey的处理方法。
Redis 6.0引入了多线程I/O,把网络读写拆分到了额外的线程池,但命令执行仍是主线程单线程。Redis 8.0开始支持命令执行多线程,算是一个架构上的大变化,有兴趣可以在测试环境研究。
2.3 数据都在内存里,持久化是最后的兜底
Redis数据放内存,所以快。但机器重启、进程崩溃的时候,内存数据会全部消失。这是学习Redis必须接受的第一性原理:没有开启持久化,Redis就是一个带高级数据结构的内存缓存。
持久化方案有RDB、AOF和二者的混合模式,细节放到后面专门讲。这里只想建立一个观念:使用Redis之前,先问自己一个问题——“如果这个Redis实例重启了,数据丢了能接受吗?”
- 纯缓存场景,业务数据都在数据库里,丢了缓存无非是重新回源加载,可以接受。
- 库存、订单、分布式锁等关键数据,一旦丢了就是事故,必须开启AOF,甚至要做高可用。
想清楚这个问题,你才知道该往redis.conf里写什么。
3. 五大基础数据类型逐一说透:命令、场景与底层结构
Redis学习绕不开的核心,就是五大基础数据类型。光记住命令没有用,得知道每种结构面对什么业务场景、底层怎么实现、坑在哪。
3.1 String:最基础也最容易滥用
String是Redis最简单的结构,value最大能到512MB。常用命令包括SET、GET、SETNX、INCR、DECR、EXPIRE、TTL。这里有几个典型场景:
- 缓存:直接把数据库查询结果的JSON序列化后存入String。
- 计数器:
INCR自增天然线程安全,做点赞数、播放量、PV统计非常顺手。 - 分布式锁:
SET key value NX PX 30000,这个后面实战章节会展开。 - 分布式ID:
INCRBY key 1000生成一批ID,再在应用层分配。
底层实现上,String用的是一种叫SDS(Simple Dynamic String)的结构,相比C语言的字符串,它记录了长度信息,取长度是O(1)的,而且因为存储了len,二进制安全。Redis会根据value的长度和内容选择编码方式:短整数用int编码,短字符串用embstr,超过一定阈值则用raw。
大量新手会踩的坑是:把一个特别大的对象直接SET进Redis,比如把用户的完整订单列表JSON序列化后塞进去。String本身能放下,但后续每次对这个key做操作都可能触发大key问题,传输慢、阻塞高,还容易把网络带宽打满。后面会专门讲BigKey的危害与排查。
3.2 Hash:对象模型的最佳搭档
Hash是一个field-value映射集合,适合表示对象。比如用户信息:
HSET user:1001 name "张三" age 28 city "北京" HGET user:1001 name HGETALL user:1001和把整个对象序列化成String相比,Hash最大的优势是可以单独更新某个字段。用户改了头像,只需要HSET user:1001 avatar "new.jpg",不用把整个对象读出来改完再写回去。这在并发场景下能减少很多数据竞争问题。
Hash内部编码有两种:field数量少且值小的时候用listpack(7.0之前是ziplist),省内存;field多或者某个value大时自动转为hashtable。学习中可以用OBJECT ENCODING user:1001查看当前key用的编码,这是个很好的理解底层的方式。
购物车是Hash的经典应用:HSET cart:1001 sku_001 2,加购就是HINCRBY,查询HGETALL,删除HDEL。字段是商品ID,值是数量,语义非常清晰。
3.3 List:顺序列表与阻塞队列
List是双向链表结构,支持从头部和尾部操作。常用命令:LPUSH、RPUSH、LPOP、RPOP、LRANGE、LLEN、BRPOP。
最经典的场景有两个:
- 最新消息列表:
LPUSH新内容,LRANGE list 0 9取最新10条。微博/朋友圈feed流这种时间线场景,用List做分页非常合适。 - 简单工作队列:
LPUSH任务,消费者BRPOP阻塞弹出。BRPOP key 0的含义是:如果列表为空,就阻塞等待,直到有新元素进来。这个命令让Redis能在不引入消息中间件的情况下实现一个轻量级队列。
底层实现上,List用的是quicklist结构,本质是“链表+压缩节点”的组合。7.0以后每个节点用listpack存储多个元素,既保证了两端操作的高效,又控制了内存占用。
缺点是List做队列没有消息确认机制,消费者LPOP拿到任务后如果处理失败,消息就丢了。真要追求可靠消息,要么引入Stream(下面会讲),要么直接用专业的消息队列。
3.4 Set:去重与关系运算
Set是唯一元素的无序集合。常用命令:SADD、SREM、SMEMBERS、SISMEMBER、SCARD,以及集合运算SINTER(交集)、SUNION(并集)、SDIFF(差集)。
直接说场景:
- 标签系统:一篇博客打多个标签,每个标签是一个Set,集合里存文章ID。
SINTER可以找到同时包含多个标签的文章。 - 共同好友:两个用户各自的好友集合求交集。
- 抽奖去重:
SADD pool user_001,抽奖时SPOP随机弹出。 - 点赞/收藏:
SADD post:123:liked user_001,判断SISMEMBER。
Set底层在元素都是整数且数量不多时用intset编码,极端省内存;当元素不是整数或数量变大时转为hashtable。所以不要把大对象塞进Set当集合用,那会把hashtable弄得很大。
3.5 ZSet:排行榜的实现神器
ZSet,也就是有序集合,每个member关联一个score,按照score排序。命令包括ZADD、ZINCRBY、ZRANGE、ZREVRANGE、ZSCORE、ZRANGEBYSCORE、ZREM。
排行榜是ZSet最完美的应用场景。比如积分榜:ZADD rank 100 user_001,分数变化时ZINCRBY rank 5 user_001,查前10名ZREVRANGE rank 0 9 WITHSCORES。整个过程全部在Redis内完成,性能极高。
ZSet还能做延迟队列:把时间戳作为score,ZADD delay_queue task_001 1691234567,消费者用ZRANGEBYSCORE delay_queue 0 now LIMIT 0 1取出到期任务,处理完ZREM删除。比List队列更灵活,因为你能看到“还有多久到期”的元素。
底层实现是跳表(skip list)加哈希表。跳表是个多层级链表,查询复杂度O(logN),代码实现比红黑树简单,但表现接近。如果你想深入Redis,跳表值得花时间研究一下,很多面试也从这里出题。
4. 扩展数据类型:从Bitmap到Stream,每个都能解决一类实际问题
五大基础类型是入门,但实际业务里经常需要更“专”的结构。Redis提供的几个扩展数据类型,用好了能省大量存储和代码。
4.1 BitMap:用一个bit统计一个用户
Bitmap不是独立的数据结构,本质上是String类型上的一组位操作。命令包括SETBIT、GETBIT、BITCOUNT、BITPOS。
最典型的场景是用户签到。一年365天,每天给用户分配一个bit位,0表示未签到,1表示已签到。SETBIT user:sign:2025 100 1表示第101天签到了,BITCOUNT user:sign:2025统计全年签到天数。1亿用户做一年签到统计,大概只需要1亿×365/8字节,也就是几个GB级别,相比存365个Hash字段省得多。
还有一个经典用法是统计在线用户。用户上线时SETBIT online 1001 1,定时BITCOUNT online就是当前在线人数,下线SETBIT online 1001 0。
4.2 HyperLogLog:用极小的空间统计UV
如果只需要知道“去重后的数量”,不关心具体是哪些用户,HyperLogLog是最合适的选择。命令只有三个:PFADD、PFCOUNT、PFMERGE。
它的原理是基于概率统计,每个key固定占用约12KB内存,却能统计2^64级别的去重数。代价是有0.81%左右的标准误差。对于UV统计这种精度需求不算苛刻的场景,完全够用。
我做过一个埋点统计系统,用PFADD uv:2025-08-01 user_001记录每日访客,PFCOUNT uv:2025-08-01出报表。换MySQL的方案需要一张天量的明细表,换Redis Set方案内存会膨胀,HyperLogLog是性价比最高的。
4.3 Geo:附近的人与门店
Redis 3.2引入的Geo类型,底层基于ZSet,把经纬度编码成score。命令有GEOADD、GEOSEARCH、GEODIST。
做“附近的门店”功能时,把门店坐标GEOADD shop:location 116.40 39.90 store_001,查询时GEOSEARCH shop:location FROMLONLAT 116.35 39.85 BYRADIUS 5 km ASC就能按距离从近到远返回门店列表。底层的geohash编码把二维坐标转成一维字符串,相邻位置的前缀也相近,所以能高效做范围检索。
这个功能如果自己实现,得引入一个空间索引库,复杂度不低。Redis内置Geo以后,小规模场景直接就能用。
4.4 Stream:Redis原生消息队列
Stream是Redis 5.0引入的日志型消息结构,解决了List做消息队列时“消息没有确认机制、无法多消费者组”的痛点。命令有XADD、XREAD、XGROUP CREATE、XREADGROUP、XACK。
Stream和Pub/Sub最大的区别是:Pub/Sub的消息是即发即焚,消费者不在线消息直接没了;Stream则把消息持久化在Redis里,消费者可以按需读取,读取后通过XACK确认处理成功。它天然支持消费者组,一条消息可以被组内多个消费者分担处理,也可以通过XREADGROUP GROUP指定消费。
如果一个项目里不想引入Kafka/RabbitMQ,业务量又不算大,Stream完全可以充当一个可靠的消息队列。但要认识到它仍是单机Redis的能力,做分布式的消息集群不是一个简单的话题。
4.5 模块生态:布隆过滤器等“外挂”
Redis的模块机制让它能扩展更多数据结构,最常用的就是RedisBloom提供的布隆过滤器。命令是BF.ADD、BF.EXISTS。
布隆过滤器解决的问题是:快速判断一个key“一定不存在”还是“可能存在”。它用多个哈希函数对元素映射到bit数组,查询时只要有一个bit为0就说明肯定不存在,全部为1只能说可能存在。用在缓存穿透场景下,可以在请求Redis之前先查布隆过滤器,拦截掉那些肯定不存在于数据库中的非法key。
使用模块的方式是下载并加载:loadmodule /path/to/redisbloom.so。Redis Stack版本则直接集成了BloomFilter、Search、JSON、TimeSeries等模块,开发环境用起来非常方便。
5. 数据不丢的三道防线:过期删除、内存淘汰与持久化
Redis的运维属性和工程属性都在这一章。不理解过期和淘汰机制,内存被写爆了都不一定知道原因;不理解持久化,重启丢数据只会一脸懵。
5.1 过期删除:为什么内存还是居高不下
EXPIRE key seconds设置过期时间,TTL key查看剩余秒数。很多人以为设置了过期时间,数据到点就会被立刻清掉,但Redis默认采用惰性删除 + 定期删除的组合策略。
惰性删除是当key被访问时,发现已过期就直接删除并返回空;定期删除是每隔一段时间抽样一部分带过期时间的key,把到期了的删除。这样做是为了避免“每秒遍历所有key”带来的CPU开销,但也导致一个现象:过期的key不会立刻从内存消失,内存占用在过期高峰后会滞后下降。
理解这个机制后就能解释很多问题:为什么设置了TTL,used_memory还是只涨不跌;为什么明明没多少有效数据,内存却一直很高。应对办法是,如果系统对内存回收有硬性要求,可以考虑主动SCAN遍历并删除过期key,或者直接调低淘汰策略的触发水位(下面讲)。
5.2 内存淘汰:写满之后会发生什么
Redis配置里maxmemory限制最大可用内存,maxmemory-policy决定内存写满后怎么办。
默认策略是noeviction,意思是不再接受写请求,直接返回OOM错误。这在不允许误删数据的场景是对的,但缓存场景根本没法用——缓存全堆满了写不进去,等于服务雪崩。
常用策略:
allkeys-lru:从所有key中淘汰最久没被访问的key,适合纯缓存场景。volatile-lru:只从设置了过期时间的key里淘汰最久没被访问的,适合“一部分数据要长期保留,一部分允许淘汰”的混合场景。allkeys-lfu:按访问频率淘汰,比LRU更精准,能防住“偶发热点刷掉老热点”的问题。volatile-ttl:优先淘汰剩余过期时间最短的key。
我自己的实践是,缓存实例通常设maxmemory-policy allkeys-lfu,因为缓存本来就是允许丢失的,LFU比LRU更能准确反映真实热门程度。
5.3 持久化:RDB、AOF和混合模式怎么选
RDB是周期性生成数据快照,执行BGSAVE后由fork出的子进程把全量数据写入磁盘文件。优点是文件紧凑、恢复速度快,缺点是快照之间丢数据。触发条件由save配置控制,比如默认的save 900 1表示900秒内有1次写操作就保存一次。RDB在fork子进程时如果内存数据量非常大,主进程创建子进程的一瞬间会有短暂的阻塞,这是运维层需要关注的点。
AOF是把每次写命令追加到日志文件,重启时回放日志恢复数据。appendfsync有三个级别:always每个命令都刷盘,最安全但慢;everysec每秒刷一次盘,是性能和安全的平衡点;no交给操作系统决定刷盘时机,最快但丢数据可能性最大。AOF文件会无限增长,所以需要BGREWRITEAOF重写压缩,重写时会生成当前数据的最小命令集。
混合持久化是Redis 4.0之后的方案,aof-use-rdb-preamble yes开启后,AOF重写产生的文件开头是RDB格式的全量快照,后面追加增量命令。好处是恢复快、文件体积小、丢数据窗口小。也是我目前的默认选择。
部署时的建议很简单:要么不开(架构允许),要么开混合持久化加appendfsync everysec。既不要盲目把所有写都刷盘(性能下降明显),也不要完全不持久化(数据丢失无兜底)。
6. 从踩坑到防坑:BigKey、慢查询与三大缓存故障
这一节是我最想写给初学者的。Redis学习到一定阶段,命令和数据类型的知识都好掌握,真正区分水平的是线上问题的排查和预防能力。
6.1 BigKey和HotKey到底怎么搞
BigKey指单个key对应的value过大,比如一个String超过10KB,或者一个Hash/List/ZSet里的元素超过几千个。危害有两个:
- 操作慢:网络传输就需要大量时间,单线程下会阻塞其他命令。
- 内存不均:Redis集群分片时按key分配,某个大key会落到某个节点,导致节点内存和CPU都成为瓶颈。
发现BigKey最快的方式是用redis-cli --bigkeys,它会用SCAN遍历并统计各类结构最大的key。更精确的方式是写脚本遍历key,对String执行STRLEN,对集合类型执行HLEN/LLEN/SCARD/ZCARD,把超大key揪出来。
处理思路也是三板斧:拆,把一个Hash大key按业务维度拆成多个小key;压,把大JSON用压缩算法或者按字段精简;删,删除大key时用UNLINK而不是DEL,UNLINK是异步释放内存,不会阻塞主线程。
HotKey则是指某个key被高频访问。比如一个热门活动的配置。单点到同一个实例后,这台机器打满,其他机器很闲。解决方式:在应用层加一层本地缓存(比如Caffeine),兜住大部分热点请求;或者把同样的数据复制成多份key,product:399散列成product:399:0到product:399:9,把请求分散到不同分片。
6.2 慢查询日志怎么配置和阅读
Redis提供了一套慢查询日志机制:
CONFIG SET slowlog-log-slower-than 10000 CONFIG SET slowlog-max-len 128 SLOWLOG GET 10slowlog-log-slower-than单位是微秒,10000就是10毫秒。执行时间超过10毫秒的命令会被记录。slowlog-max-len是保存条数。出现慢查询后,SLOWLOG GET能看到是哪条命令、哪个客户端、什么时间执行的。
实际线上,Redis慢查询九成来自三类:KEYS、SMEMBERS/HGETALL大集合全量读取、以及BigKey上的大量写操作。KEYS必须替换成SCAN,SMEMBERS改成分页取,这些都是基本操作。
6.3 缓存穿透、击穿、雪崩的成因与应对
这三个词是Redis面试必问,也是线上事故重灾区。我不念定义,直接讲成因和方案。
穿透是查一个必定不存在的key,缓存没有,数据库也没有,每次请求都直达数据库,相当于缓存被绕过。恶意攻击尤其喜欢用不存在的ID打穿缓存。解法有两个层次:第一层用布隆过滤器在缓存之前拦住不存在的key;第二层把“查不到”也缓存一份短时间数据,比如SET empty:user:999 null EX 60,避免持续打数据库。
击穿是某个热点key恰好到点过期,大量请求同时发现缓存为空,全部回源数据库。必须靠并发控制,比如热点key加一个互斥锁,只有一个请求允许去数据库加载并回写缓存,其他请求等待或者用旧值兜底。或者让热点数据“逻辑不过期”,后台线程定期刷新,缓存里只有极短的逻辑过期时间。
雪崩是大量key同时过期,或者Redis直接宕机。大量key同时过期的解法很简单:过期时间加一个随机抖动。比如基础TTL是3600秒,实际设置成3600 + random(0, 600),把集中失效打散。Redis宕机这个层面的方案是主从加哨兵、Cluster集群和高可用架构,这是运维层面的事,但应用层也要做好降级和限流,不然数据库会被瞬间打垮。
7. 落到项目里:缓存一致性、分布式锁与秒杀库存
学Redis的最终目的是写进业务。我从自己做过的高并发项目里挑三个高频场景,讲讲正确写法和容易翻车的地方。
7.1 缓存与数据库的双写一致性
最常见的缓存模式是Cache Aside:读时先查缓存,miss了读数据库,再把数据写进缓存;更新时先更新数据库,再删除缓存。这里关键是为什么“删缓存”而不是“更新缓存”。
更新缓存的问题是,如果两个并发请求同时更新数据,后写数据库的请求可能先写缓存,或者先写缓存的请求被后写数据库的数据覆盖,最终缓存里留下过期数据。删除缓存则不同,下次读请求会拉取最新数据重建缓存,即使并发也只会多一次数据库查询。
但“先更新数据库,再删缓存”也会有一个经典窗口:线程A读缓存miss,去数据库读旧值,线程B更新数据库并删缓存,线程A再把旧值写回缓存。解决思路之一是延迟双删:更新数据库后删除缓存,再等几百毫秒(比如500ms)删除一次。第二次删除的目的就是清掉那个被旧值写回的缓存。实际项目中还可以用消息队列异步删缓存,或订阅MySQL binlog同步删除,都能做到更严格的最终一致。
7.2 分布式锁的正确姿势
Redis分布式锁是用了最多次也最容易写错的。正确加锁方式一条命令:
SET lock:order "uuid-123" NX PX 30000NX保证只有key不存在时才能设置成功,PX设置过期时间防止持锁进程崩溃导致死锁,value必须是唯一标识(比如UUID),用于释放时确认是自己持有的锁。释放锁必须用Lua脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end用Lua的原因很简单:GET判断和DEL必须原子执行。如果先GET再DEL,中间可能被别人的锁覆盖。value里的UUID就是为了防止误删:A的锁过期了,B拿到了锁,A执行DEL把B的锁删掉了。
开源客户端Redisson把加锁、续期、释放都封装好了,还带了“看门狗”机制,默认每10秒自动续期到30秒,避免业务没执行完锁就过期。自己写分布式锁要很谨慎,能用成熟库就不要重复造轮子。
7.3 秒杀库存:Lua脚本保证原子扣减
秒杀场景下的库存扣减,要求并发环境下不能超卖。Redis单线程执行命令的特性让原子扣减变得很优雅。先把库存预热到Redis:
SET stock:1001 100扣减库存使用Lua脚本:
local stock = tonumber(redis.call("get", KEYS[1])) if not stock then return -1 end if stock <= 0 then return 0 end redis.call("decrby", KEYS[1], 1) return 1因为Lua脚本在Redis中是原子执行的,多个线程同时执行这个脚本时,Redis会挨个执行,不存在同时读到相同库存的情况。脚本返回1表示扣减成功,返回0表示库存不足,返回-1表示key不存在。
我自己做秒杀时,会把库存扣减和资格写入分开。扣减成功后把用户ID写入一个Set占位,再发消息给下游做订单创建。整个过程在Redis层就完成了高并发控制,数据库只接收最终已经扣减成功的订单请求,压力小很多。
8. 一条实用的Redis学习路径和工具清单
最后分享一点学习路线层面的建议。Redis入门门槛低,但深入需要花费的时间不少。如果你是从零开始,我的建议顺序是这样。
第一阶段先把基础命令过一遍,彻底理解String、Hash、List、Set、ZSet的使用场景。不要死记命令,每学一个类型就想一个业务问题能用它解决。我给自己的练习方式是拿朋友圈功能练手:用户时间线用List,用户资料用Hash,点赞关系用Set,热度排行用ZSet。
第二阶段去研究底层实现。SDS为什么比C字符串安全,ziplist和listpack有什么差别,跳表是怎么做到O(logN)的,字典的rehash过程是怎样的。这些知识短期看起来不影响写代码,但排查性能和内存问题的时候非常有用。
第三阶段理解持久化、过期、淘汰、主从复制、哨兵、集群。不需要亲手搭一个生产级集群,但至少要在本地用docker compose把一主二从三哨兵跑起来,看看主从切换时发生什么。
第四阶段就是实战和故障演练。往Redis里灌数据,人为制造BigKey,观察慢查询;设置极端小的maxmemory,看淘汰策略表现;敲DEBUG SEGFAULT模拟崩溃,检查持久化恢复。
工具方面,官方RedisInsight是必装的图形客户端,看key结构、时长曲线、慢查询都很直观。命令行工具iredis支持命令补全和语法高亮,写脚本调试时比redis-cli好用。排查RDB文件可以用redis-rdb-tools。压测就老老实实用redis-benchmark,它自带了一些常用场景的基准测试模板,能看到机器上的真实吞吐。
我自己的体会是,Redis学习性价比很高,它把非常多经过实践验证的系统设计思路浓缩在了一个简单的结构里。不要停留在“会用”,多问几个“为什么”,多模拟几次故障,等你真正理解了它为什么快、为什么数据不会丢、为什么某些操作会阻塞,线上遇到的绝大多数问题就都能自己推导出答案了。