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两种底层编码
- ziplist(压缩列表,小hash的时候)
当同时满足:
- field数量不多(默认 hash‑max‑ziplist‑entries 512)
- 每个field、value字节都不大(默认 hash‑max‑ziplist‑value 64字节)
底层用ziplist压缩列表存储。
ziplist是连续内存数组:按顺序存放: field1,value1,field2,value2 。
内存紧凑,节省内存。
一旦超出上面任意阈值,Redis自动转成 hashtable 编码。
- 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:100Set集合,无序、元素不可重复
# 添加元素 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 setBRedis中的三个经典问题
缓存穿透
- 问题:查询一个数据库里根本不存在的数据,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、过期时间、扣减数量等,不是keyredis.call(‘SET’, key, val)
redis.call() :调用Redis命令。第一个字符串是redis命令名,后面是命令参数。
等价执行: SET key val
如果这条redis命令报错,整个脚本直接终止,后面代码不再运行。不会回滚的
对比: redis.pcall(‘SET’,key,val)
pcall遇到错误不会终止脚本,会把错误信息返回,脚本继续向下跑。开发大部分场景用 callreturn 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),释放锁时验证"是不是自己的锁",防止误删别人的锁
详解
核心目标:
- 互斥:同一时间,只能有一个客户端拿到锁
- 防死锁:锁一定要有过期时间,客户端挂了锁要能自动释放
- 解铃还须系铃人:谁加锁,谁才能释放锁;不能把别人的锁释放掉
- 可重入(可选):同一个线程可以多次获取同一把锁
- 高可用: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秒
- 如果业务执行完毕,线程退出,不再续期;锁到期自动释放