news 2026/10/28 10:00:06

Redis缓存与分布式锁的基础知识点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis缓存与分布式锁的基础知识点

Redis快的原因

  • 纯内存操作,没有磁盘IO
  • 单线程模式,没有多线程的上下文切换
  • IO多路复用,一个单线程监听所有网络连接
  • 高效的数据结构

如果Redis是多线程的话

  • 多线程操作共享数据,必须加锁。多个线程同时写同一个key,会出现并发错乱。锁会带来:锁竞争、死锁、锁等待,代码复杂度暴涨
  • 线程的上下文切换开销大
  • Redis核心工作是内存读写,不消耗CPU,单线程就足够且天然线程安全,没有上下文开销

IO多路复用的解释

  • 没有多路复用的话,一个客户端连接,就需要一个线程去等待数据,数据通过网络发过来,比较慢,线程就在那等着
  • Redis把全部的socket的链接都交给操作系统,数据发过来了就唤醒Redis的线程来处理

Redis里面的常用数据结构和语法

String 可存字符串、数字

  • 应用场景:缓存、计数器、分布式锁
# 设置key value set key value # 设置同时设置过期时间,单位秒 set key value ex 10 # 不存在才设置(分布式锁用) setnx key value # 获取 get key # 删除 del key # 数字自增 incr key # 增加指定数值 incrby key 5 # 自减 decr key # 批量设置 mset k1 v1 k2 v2 # 批量获取 mget k1 k2
  • INCR 这个操作在 Redis 内部是"读取→加1→写回"三步,但 Redis 单线程保证这三步不会被其他命令打断,要么全部执行完,要么不执行,不存在两个请求同时读到 0、都加 1、结果都写成 1 的并发问题。这就是原子性,是后面限流和分布式锁的基础

Hash

  • 存储方法hset key field value
  • 对于hset user:100 name zhangsan age 20
  • Redis key:user:100(整个hash的大key)
  • field:name、age
  • value:zhangsan、20
user:100 { name → zhangsan age → 20 }
Hash两种底层编码
  1. ziplist(压缩列表,小hash的时候)
    当同时满足:
  • field数量不多(默认 hash‑max‑ziplist‑entries 512)
  • 每个field、value字节都不大(默认 hash‑max‑ziplist‑value 64字节)
    底层用ziplist压缩列表存储。
    ziplist是连续内存数组:按顺序存放: field1,value1,field2,value2 。
    内存紧凑,节省内存。
    一旦超出上面任意阈值,Redis自动转成 hashtable 编码。
  1. hashtable(字典dict,大hash)
    当hash的field变多、或者字段value变长,自动升级为 dict(哈希表)。
    底层就是Redis自己实现的哈希字典,和Java HashMap类似:数组+链表,发生hash冲突链表挂链。
  • 查询复杂度 O(1)
  • 内存开销会比ziplist大。
    不需要手动切换编码,Redis自动做。
总结
  • Redis Hash,外层是Redis大key,内部存储多组field‑value
  • 底层有两种编码:数据量小时用ziplist压缩列表,连续内存节省空间
  • 当字段数量或者value大小超过阈值,自动转为dict哈希表
  • Hash只能给整个key设置过期,无法单独给field设置过期时间
  • 适合存储对象,支持局部字段更新
操作语法
# 设置字段 hset user:100 name zhangsan age 20 # 获取单个字段 hget user:100 name # 获取全部field‑value hgetall user:100 # 获取多个字段 hmget user:100 name age # 判断field是否存在 hexists user:100 name # 删除某个字段,不是删整个key hdel user:100 age # 字段数值增加 hincrby user:100 age 1 # 获取所有field hkeys user:100 # 获取所有value hvals user:100

Set集合,无序、元素不可重复

# 添加元素 sadd set1 a b c a # 获取全部元素 smembers set1 # 判断是否存在 sismember set1 a # 删除元素 srem set1 b # 获取集合大小 scard set1 # 随机弹出一个元素 spop set1 # 差集、交集、并集 sdiff setA setB sinter setA setB sunion setA setB

Redis中的三个经典问题

缓存穿透

  • 问题:查询一个数据库里根本不存在的数据,Redis 没有,MySQL 也没有,每次请求都打到 MySQL。攻击者用大量不存在的 id 请求,压垮数据库
解决方法
  • 缓存空值(缓存null)
  • 数据库查询为空,仍然往Redis写一个key,value=null,设置短过期时间(比如3‑5分钟)
  • 下次再查,直接返回缓存null,不会访问数据库。
  • 缺点:会存很多无用key,占用内存
  • 伪代码逻辑
// 查询缓存Stringcache=redis.get("goods:100");if(cache!=null){returncache;}// 查数据库Goodsgoods=mapper.selectById(100);if(goods==null){redis.set("goods:100","null",3,TimeUnit.MINUTES);//缓存空,短过期returnnull;}redis.set("goods:100",JSON.toJSONString(goods),30,TimeUnit.MINUTES);returngoods;
  • 布隆过滤器 BloomFilter(生产常用)
  • 把所有合法存在的业务id,全部预加载进布隆过滤器。
  • 请求过来,先过布隆过滤器:
  • 布隆说不存在 → 直接返回,不去查Redis、数据库
  • 布隆说存在 → 才继续查缓存、数据库
  • 特点:布隆过滤器存在误判,不会漏判
  • 说不存在一定不存在;说存在有可能实际不存在
  • 适合海量数据拦截恶意id。

缓存穿透

  • 问题:查询一个数据库里根本不存在的数据,Redis 没有,MySQL 也没有,每次请求都打到 MySQL。攻击者用大量不存在的 id 请求,压垮数据库
解决方案
  • 互斥锁(分布式锁)
  • 缓存失效的时候,只放行一个线程去查数据库写缓存;其他线程等待或者重试
伪流程: - 缓存未命中 - 获取分布式锁,拿到锁的线程去查DB,回写缓存 - 没拿到锁的线程:sleep一小段时间重试读缓存

  • 热点key永不过期(逻辑过期)
  • Redis不设置TTL过期时间。
  • 在value里面存一个逻辑过期时间字段。
  • 所有请求直接返回缓存数据;单独开一个后台异步线程,定时刷新缓存。
  • Redis 中的 key 物理上永不过期,是否失效由代码中的逻辑过期时间来判断
  • 缺点:数据在刷新窗口期内可能会有短暂的不一致。

  • 提前主动刷新热点key
  • 热点数据在快要过期前,程序主动更新缓存,不让它过期。

缓存雪崩

  • 问题:查询一个数据库里根本不存在的数据,Redis 没有,MySQL 也没有,每次请求都打到 MySQL。攻击者用大量不存在的 id 请求,压垮数据库。
解决方案
  • 过期时间加随机值
  • 在原有TTL基础上增加随机偏移量
  • 例: 30分钟 + 随机0~5分钟 ,避免大批量key同一时刻过期

  • 哨兵

  • 服务熔断和降级

Lua脚本

  • Redis 支持执行 Lua 脚本。你可以把多条 Redis 命令写进一个 Lua 脚本,一次性发给 Redis 执行,保证了原子性,减少了网络开销
  • 一般用在限流里面
  • 如果限流不用Lua脚本
Long count = redisTemplate.opsForValue().increment(key); // 步骤1:计数 if (count == 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); // 步骤2:设置过期 }
  • 问题:步骤1和步骤2之间,如果服务器突然宕机,INCR 执行了但 EXPIRE 没执行,这个 key 就永不过期,限流计数器永远不归零,这个用户就被永久限流了。而且高并发下"判断 count==1"也有并发问题

先解释脚本语法

-- test.lua local key = KEYS[1] local val = ARGV[1] redis.call('SET', key, val) return redis.call('GET', key)
  • – test.lua
    – 是lua的注释,后面内容不会执行。
    ​

  • local key = KEYS[1]
    local :lua声明局部变量,只在当前脚本内生效。不加local就是全局变量
    ​ KEYS :Redis传给Lua脚本的专用数组,专门存redis的key。
    ​

  • KEYS[1] :取传入的第1个key;lua数组下标从1开始,不是0!这点非常容易踩坑
    不要写 KEYS[0] ,拿不到数据。

  • local val = ARGV[1]
    ARGV :同样是Redis内置数组,存放普通参数(value、数字,不是redis的key)。
    ​ARGV[1] 获取传入的第一个参数值。
    KEYS[] → 存Redis的key(要操作的redis键)
    ARGV[] → 普通参数,value、过期时间、扣减数量等,不是key

  • redis.call(‘SET’, key, val)
    redis.call() :调用Redis命令。第一个字符串是redis命令名,后面是命令参数。
    等价执行: SET key val
    如果这条redis命令报错,整个脚本直接终止,后面代码不再运行。不会回滚的
    对比: redis.pcall(‘SET’,key,val)
    pcall遇到错误不会终止脚本,会把错误信息返回,脚本继续向下跑。开发大部分场景用 call

  • return redis.call(‘GET’, key)
    把get拿到的结果,作为脚本返回值,交给Java/客户端

怎么传KEYS和ARGV
redis-cli--evaltest.lua name , zhangsan
  • test.lua :脚本文件
  • name → 对应 KEYS[1] ,这是redis key
  • , 分隔符!逗号前后必须有空格,逗号左边全部是KEYS,逗号右边全部是ARGV
  • zhangsan →对应 ARGV[1] ,value
  • 脚本执行逻辑: SET name zhangsan ,然后 GET name ,返回 zhangsan

分布式锁

  • 解决并发问题时Java 单机可以用 synchronized 或 ReentrantLock 加锁,保证同一时间只有一个线程执行抽奖。但这是单机锁——只能锁住当前这台服务器
  • 分布式场景:如果项目部署了多台服务器(负载均衡),请求 A 打到服务器1,请求 B 打到服务器2,两台服务器各有各的 JVM, synchronized 根本锁不住对方
  • 分布式锁就是把锁存在一个所有服务器都能访问的公共地方(Redis),谁先抢到谁执行,其他服务器等待
SET key value NX EX 30
  • NX (Only set if Not eXists):key 不存在时才设置成功(相当于抢锁),存在则失败
  • EX 30 :30 秒自动过期(防止拿到锁的服务器宕机,锁永远不释放 → 死锁)
  • value :存唯一标识(如 UUID),释放锁时验证"是不是自己的锁",防止误删别人的锁

详解

核心目标:

  1. 互斥:同一时间,只能有一个客户端拿到锁
  2. 防死锁:锁一定要有过期时间,客户端挂了锁要能自动释放
  3. 解铃还须系铃人:谁加锁,谁才能释放锁;不能把别人的锁释放掉
  4. 可重入(可选):同一个线程可以多次获取同一把锁
  5. 高可用:Redis集群环境锁依然可靠
注意点
  • setnx和expire写成一条原子命令
set lock:order uuid-xxx NX EX 30
  • value用唯一的UUID防止别人删了这个锁
  • 删锁的时候判断value是不是自己的UUID
-- 释放锁 lua脚本ifredis.call('GET',KEYS[1])==ARGV[1]thenreturnredis.call('DEL',KEYS[1])elsereturn0end
看门狗机制
  • 解决业务时间>锁过期时间
  • 看门狗不要自己写,直接用Redisson封装的
  • 获取锁成功后,启动一个后台守护线程
  • 每 leaseTime/3 (默认10秒)检查一次:
  • 如果业务线程还持有锁,就把锁过期时间重新刷到30秒
  • 如果业务执行完毕,线程退出,不再续期;锁到期自动释放
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 9:59:52

git-spice 常见问题排查手册:10个典型错误与故障解决方案清单

【免费下载链接】git-spice Manage stacked Git branches 项目地址: https://gitcode.com/gh_mirrors/gi/git-spice 点击查看 免费下载 git-spice 是一款用于管理 Git 堆叠分支(stacked Git branches) 的命令行工具,支持分支栈的…

作者头像 李华
网站建设 2026/10/28 9:59:08

大模型 API 与订阅套餐比价指南:从定价规则到成本试算

做一个大模型 API 和订阅套餐的比价目录,这个想法其实一开始挺冲动的。我当时的处境是:项目要控制成本,得从一堆国内大模型厂商里选一个主力 API,结果把各个官网的价格页打开对着看了整整一个下午,越看越头大——有的按…

作者头像 李华
网站建设 2026/10/28 9:56:03

Linux命令从入门到精通:系统管理与运维实战指南

1. Linux命令概述:从入门到精通的系统管理基石作为一位与Linux系统打交道超过15年的老运维,我始终认为命令行是Linux最强大的武器。那些看似晦涩的字符串组合,实则是与计算机对话的最高效方式。记得2008年我第一次接触ls命令时的震撼——原来…

作者头像 李华
网站建设 2026/10/28 9:44:39

校园二手交易小程序源码拆解:微信登录、手机号授权与避坑指南

简介:基于微信小程序的校园二手交易平台源码包,后台采用微信云开发,实现租赁与买断两种交易模式,适合正在学习小程序开发、准备课程设计或毕业设计的同学参考。整套资源共9个文件,其中6个json为项目配置,1个…

作者头像 李华
网站建设 2026/10/28 9:39:53

Text2SQL选型推荐矩阵:五层方案、四维决策与12场景落地指南

如果有人问我,Text2SQL落地最痛苦的一步是什么,我大概率会回答:不是模型选不好,而是方案选不定。过去大半年我帮好几支团队做Text2SQL的技术评审和落地,他们递过来的Demo一个比一个好看——随便问一句就能出SQL&#x…

作者头像 李华