文章目录
- Redis核心命令以及技术方案参考文档(分布式锁,缓存业务逻辑)
-
- redis基础命令操作
-
- [一、Redis 通用命令](#一、Redis 通用命令)
- [二、Redis 基本命令](#二、Redis 基本命令)
- [三、Redis 五种数据结构命令](#三、Redis 五种数据结构命令)
- redis分布式锁
-
- 一、先明确分布式锁的核心诉求
- 二、加锁逻辑(核心)
- 三、解锁逻辑:最容易踩坑的一步,原子性是关键
-
- 1.错误的解锁方式(新手最容易犯的错)
- [2.正确的解锁方式:Lua 脚本保证原子性](#2.正确的解锁方式:Lua 脚本保证原子性)
- 3.解锁的完整流程
- 四、重试逻辑:平衡用户体验和系统压力
- 五、完整的分布式锁实现
- 缓存业务存在的核心问题
-
- 一、缓存穿透:防护方案(从易到难)
- [二、缓存击穿:防护方案(针对热点 key)](#二、缓存击穿:防护方案(针对热点 key))
- 三、缓存雪崩:防护方案(系统性防护)
- 缓存的基本业务逻辑
-
- [一、 读缓存流程(Cache-Aside Pattern)](#一、 读缓存流程(Cache-Aside Pattern))
- [二、 写缓存流程](#二、 写缓存流程)
Redis核心命令以及技术方案参考文档(分布式锁,缓存业务逻辑)
redis基础命令操作
一、Redis 通用命令
| 命令 | 功能描述 | 示例 |
|---|---|---|
select <db-index> |
切换数据库(Redis 默认 16 个库,索引从 0 开始) | select 1 (切换到第 2 个数据库) |
DBSIZE |
查看当前数据库中 key 的数量 | DBSIZE |
set <key> <value> |
设置指定 key 的 value | set username mike |
get <key> |
获取指定 key 的 value | get username |
keys * |
获取当前数据库中所有 key | keys * |
flushdb |
清空当前数据库的所有数据 | flushdb |
flushall |
清空所有数据库的所有数据 | flushall |
二、Redis 基本命令
| 命令 | 功能描述 | 示例 |
|---|---|---|
exists <key> |
查询指定 key 是否存在 | exists username |
move <key> <db-index> |
将指定 key 移动到目标数据库 | move username 1 |
expire <key> <seconds> |
设置 key 的过期时间(单位:秒) | expire username 10 |
ttl <key> |
查看 key 的剩余过期时间(-1 = 永不过期,-2 = 已过期) | ttl username |
type <key> |
查看 key 对应值的数据类型 | type username |
三、Redis 五种数据结构命令
1.String(字符串)类型
| 命令 | 功能描述 | 示例 |
|---|---|---|
set <key> <value> |
设置字符串值 | set name htt |
get <key> |
获取字符串值 | get name |
append <key> <suffix> |
拼接字符串到指定 key 的 value 末尾 | append name study |
strlen <key> |
获取 value 的长度 | strlen name |
incr <key> |
数值型 value 自增 1(value 非数值会报错) | incr view |
decr <key> |
数值型 value 自减 1 | decr view |
incrby <key> <num> |
数值型 value 自增指定数值 | incrby view 10 |
decrby <key> <num> |
数值型 value 自减指定数值 | decrby view 10 |
getrange <key> <start> <end> |
截取字符串(闭区间,下标从 0 开始) | getrange name 0 3 |
setrange <key> <offset> <value> |
从指定下标替换字符串 | setrange name 1 000 |
setex <key> <seconds> <value> |
设置值并指定过期时间 | setex name 10 hello |
setnx <key> <value> |
仅当 key 不存在时设置值(原子操作) | setnx title redis |
mset <k1> <v1> <k2> <v2> ... |
批量设置多个键值对 | mset k1 v1 k2 v2 k3 v3 / mset user:1:name htt user:1:age 2 |
mget <k1> <k2> ... |
批量获取多个 key 的值 | mget k1 k2 k3 / mget user:1:name user:1:age |
msetnx <k1> <v1> <k2> <v2> ... |
批量设置(所有 key 都不存在才成功,原子性) | msetnx k1 v1 k4 v4 |
getset <key> <new-value> |
获取原 value 并设置新 value(key 不存在返回 nil) | getset username htt |
2.List(列表)类型
| 命令 | 功能描述 | 示例 |
|---|---|---|
lpush <list-key> <value> |
从列表头部插入值 | lpush list 1 |
rpush <list-key> <value> |
从列表尾部插入值 | rpush list 4 |
lrange <list-key> <start> <end> |
获取指定区间的元素(0=-1 表示所有) | lrange list 0 -1 |
lpop <list-key> |
移除并返回列表第一个元素 | lpop list |
rpop <list-key> |
移除并返回列表最后一个元素 | rpop list |
lindex <list-key> <index> |
通过下标获取列表中的元素 | lindex list 0 |
llen <list-key> |
获取列表长度 | llen list |
lrem <list-key> <count> <value> |
移除指定个数的匹配元素(count=1 移除 1 个) | lrem list 1 2 |
ltrim <list-key> <start> <end> |
截取指定区间元素并覆盖原列表 | ltrim list 1 2 |
lset <list-key> <index> <value> |
更新指定下标的元素(下标不存在报错) | lset list 0 bbb |
linsert <list-key> BEFORE/AFTER <pivot> <value> |
在指定元素前 / 后插入值 | linsert list BEFORE kkk aaa / linsert list AFTER kkk aaa |
3.Set(集合)类型(无序、唯一)
| 命令 | 功能描述 | 示例 |
|---|---|---|
sadd <set-key> <value> |
向集合添加元素 | sadd set hello |
smembers <set-key> |
查看集合所有元素 | smembers set |
sismember <set-key> <value> |
判断元素是否在集合中(返回 1 = 存在,0 = 不存在) | sismember set world |
srandmember <set-key> [count] |
随机抽取指定个数元素(默认 1 个) | srandmember set / srandmember set 2 |
spop <set-key> |
随机删除并返回集合中的一个元素 | spop set |
smove <src-set> <dst-set> <value> |
将元素从一个集合移动到另一个集合 | smove set set2 world |
sdiff <set1> <set2> |
求两个集合的差集(set1 有、set2 无) | sdiff set2 set |
sinter <set1> <set2> |
求两个集合的交集 | sinter set set2 |
sunion <set1> <set2> |
求两个集合的并集(去重) | sunion set set2 |
4.Hash(哈希)类型(键值对的集合)
| 命令 | 功能描述 | 示例 |
|---|---|---|
hset <hash-key> <field> <value> |
向哈希表添加字段和值 | hset hash username mike |
hget <hash-key> <field> |
获取哈希表指定字段的值 | hget hash username |
hmset <hash-key> <f1> <v1> <f2> <v2> ... |
批量添加哈希表字段和值 | hmset hash username jack age 2 |
hmget <hash-key> <f1> <f2> ... |
批量获取哈希表指定字段的值 | hmget hash username age |
hgetall <hash-key> |
获取哈希表所有字段和值 | hgetall hash |
hdel <hash-key> <field> |
删除哈希表指定字段 | hdel hash username |
hlen <hash-key> |
获取哈希表字段数量 | hlen hash |
hexists <hash-key> <field> |
判断哈希表指定字段是否存在 | hexists hash username |
hkeys <hash-key> |
获取哈希表所有字段名 | hkeys hash |
hvals <hash-key> |
获取哈希表所有字段值 | hvals hash |
hincrby <hash-key> <field> <num> |
哈希表数值型字段自增指定数值 | hincrby hash views 1 |
hsetnx <hash-key> <field> <value> |
仅当字段不存在时设置值 | hsetnx hash password 123456 |
5.Zset(有序集合)类型(有序、唯一,通过分数排序)
| 命令 | 功能描述 | 示例 |
|---|---|---|
zadd <zset-key> <score> <value> |
添加元素(score 为排序依据) | zadd zset 1 first |
zadd <zset-key> <s1> <v1> <s2> <v2> ... |
批量添加元素 | zadd zset 2 second 3 third 4 four |
zrange <zset-key> <start> <end> |
获取指定区间元素(按 score 升序) | zrange zset 0 -1 |
zrangebyscore <zset-key> <min> <max> |
按 score 范围获取元素(升序) | zrangebyscore zset -inf +inf |
zrangebyscore <zset-key> <min> <max> withscores |
按 score 范围获取元素并显示分数 | zrangebyscore zset -inf +inf withscores |
zrangebyscore <zset-key> <min> <max> withscores |
按指定 score 范围获取元素 | zrangebyscore zset -inf 1 withscores |
zrem <zset-key> <value> |
移除指定元素 | zrem zset four |
zcard <zset-key> |
获取有序集合元素个数 | zcard zset |
zrevrange <zset-key> <start> <end> |
按 score 降序获取指定区间元素 | zrevrange zset 1 2 |
redis分布式锁
一、先明确分布式锁的核心诉求
在分布式系统中,多台机器 / 进程同时操作同一资源(比如扣库存、创建订单),需要分布式锁保证:
-
互斥性:同一时刻只有 1 个客户端能拿到锁
-
安全性:不能误删别人的锁,也不能自己的锁被别人删
-
容错性:客户端宕机 / 网络中断,锁必须能自动释放,避免死锁
-
可用性:获取锁失败时,能合理重试,符合业务场景
二、加锁逻辑(核心)
1.加锁的核心命令拆解
加锁的核心是执行 Redis 命令:
bash
SET lock_key unique_value NX PX 30000
对应python代码:
python
redis_client.set("lock_key", "unique_value", nx=True, px=30000)
| 部分 | 作用 | 为什么必须这么设计? |
|---|---|---|
lock_key |
锁的标识(比如order:create:1001,对应订单 1001 的创建锁) |
业务维度隔离,不同业务用不同 key,避免锁冲突 |
unique_value |
每个客户端的唯一标识(通常是 UUID / 雪花 ID / 客户端 IP + 进程 ID) | 解锁时要验证这个值,确保 "谁加的锁,谁才能解",防止误删别人的锁 |
NX |
仅当 key 不存在时才设置成功(等价于 Python 的nx=True) |
核心互斥逻辑:如果 key 已存在(有人拿了锁),则设置失败,保证同一时刻只有 1 个客户端拿到锁 |
PX 30000 |
设置锁的过期时间为 30000 毫秒(30 秒,等价于 Python 的px=30000) |
容错性:如果客户端拿到锁后宕机,Redis 会自动删除过期的 key,释放锁,避免死锁 |
2.加锁的完整流程(带细节)
我用流程图展示加锁的完整逻辑,再用文字解释:
成功(返回OK)
失败(返回nil)
是
否
客户端发起加锁请求
生成唯一标识unique_value
执行SET命令:SET lock_key unique_value NX PX expire_ms
命令执行结果?
加锁成功,执行业务逻辑
进入重试逻辑
是否还有重试次数?
等待指定间隔(如500ms)
加锁失败,返回业务失败
关键细节补充:
-
unique_value 的生成规则(示例):
pythonimport uuid import os # 组合客户端IP+进程ID+UUID,确保绝对唯一,伪代码 unique_value = f"{client_ip}_{os.getpid()}_{uuid.uuid4()}"为什么不只用 UUID?------UUID 本身已足够唯一,但加客户端 / 进程信息,便于排查问题(比如日志里能看到哪个客户端拿了锁)。
-
过期时间的选择:
- 不能太短:如果业务逻辑执行需要 20 秒,锁只设 10 秒,会导致 "锁提前释放",其他客户端拿到锁,出现并发问题;
- 不能太长:如果客户端宕机,锁过期时间太长会导致 "锁迟迟不释放",影响业务可用性;
- 建议值:基于业务压测结果,取 "平均执行时间 + 冗余时间"(比如平均执行 5 秒,设 10 秒)。
-
加锁失败的直接后果:如果不加重试,直接返回失败,在高并发场景(比如秒杀)会导致 "用户点击一次,直接提示失败",体验差;但重试也不能无限制,否则会导致大量请求阻塞。
三、解锁逻辑:最容易踩坑的一步,原子性是关键
1.错误的解锁方式(新手最容易犯的错)
新手可能会这么写解锁代码:
python
# 错误示例!!!
def wrong_release(redis_client, lock_key, unique_value):
# 步骤1:先查锁的value
current_value = redis_client.get(lock_key)
# 步骤2:如果匹配,就删除
if current_value == unique_value:
redis_client.delete(lock_key)
return True
为什么错?------ 没有原子性,会导致 "误删别人的锁"
模拟一个并发场景:
| 时间 | 客户端 A | 客户端 B | Redis 状态 |
|---|---|---|---|
| T1 | 拿到锁,设置过期时间 10 秒 | - | lock_key = A 的 value,过期时间 10 秒 |
| T9 | get (lock_key) | lock_key = A 的 value,过期时间 1秒 | |
| T10 | 业务还在执行,但锁已过期 | - | Redis 自动删除 lock_key |
| T11 | delete(lock_key) | 执行 SET 命令,拿到锁 | lock_key = B 的 value,过期时间 10 秒 |
| T12 | 执行 get (lock_key),返回 B 的 value?不!啥也没有 | - | 没有锁 |
核心问题:在你get和delete 的中间是有一个延迟误差的,有可能其他的客户端用户就在这个延迟误差中间获取到了对应的锁,而这一次的delete操作就直接将这把新的锁给删了。
2.正确的解锁方式:Lua 脚本保证原子性
Lua 脚本的作用是让 "判断 value + 删除 key" 变成一个原子操作(Redis 执行 Lua 脚本时,不会被其他命令打断)。
Lua 脚本代码拆解:
lua
-- 解锁的Lua脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
-- 步骤1:判断value是否匹配(自己的锁)
return redis.call('del', KEYS[1]) -- 步骤2:匹配则删除,返回1
else
return 0 -- 不匹配则返回0,不删除
end
对应 Python 代码:
python
def release_lock(redis_client, lock_key, unique_value):
# 定义Lua脚本
unlock_script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
# 执行Lua脚本(原子操作)
# KEYS[1] = lock_key,ARGV[1] = unique_value
result = redis_client.eval(
unlock_script,
keys=[lock_key], # 传KEYS数组
args=[unique_value] # 传ARGV数组
)
# result=1:解锁成功;result=0:解锁失败(锁已过期/不是自己的锁)
return result == 1
3.解锁的完整流程
1(删除成功)
0(删除失败)
客户端执行完业务逻辑
调用解锁函数
执行Lua脚本:判断value是否等于自己的unique_value
脚本返回值?
解锁成功,锁释放
解锁失败(原因:锁已过期/被别人拿了)
关键细节补充:
-
解锁必须放在 finally 块:
无论业务逻辑是否抛出异常,都要尝试解锁,否则会导致锁被长时间持有(直到过期)。示例:
pythontry: if lock.acquire(): # 执行业务逻辑(可能抛异常) do_business() finally: # 无论是否异常,都解锁 lock.release() -
解锁失败的处理:
如果解锁返回 0(失败),不需要 panic,大概率是 "锁已过期自动释放",只需记录日志即可,不影响业务。
四、重试逻辑:平衡用户体验和系统压力
1.重试逻辑的设计原则
重试不是 "无脑循环",要满足:
- 有上限:不能无限重试,否则会导致大量请求阻塞,拖垮系统;
- 有间隔:重试间隔不能太短(比如 1ms),否则会疯狂请求 Redis,造成 Redis 压力过大;
- 可配置:重试次数和间隔要能根据业务调整(比如秒杀场景重试次数多,间隔短;普通场景重试次数少,间隔长)。
2.重试逻辑的优化(进阶)
-
指数退避重试:
重试间隔随次数增加而变长(比如第一次 0.5 秒,第二次 1 秒,第三次 2 秒),减少 Redis 压力:
python# 指数退避示例 retry_interval = 0.5 * (2 ** retry_count) # 限制最大间隔(比如不超过5秒) retry_interval = min(retry_interval, 5) -
非阻塞重试:如果是异步场景(比如消息队列消费),可以把重试逻辑放到异步任务中,不阻塞当前请求。
五、完整的分布式锁实现
python
import redis
import uuid
import time
import os
class RedisDistributedLock:
"""Redis分布式锁(兼容redis-py 4.x+版本)"""
def __init__(
self,
redis_client: redis.Redis,
lock_key: str,
expire_ms: int = 30000,
retry_times: int = 3,
retry_interval: float = 0.5,
use_exponential_backoff: bool = True # 是否开启指数退避重试
):
self.redis_client = redis_client
self.lock_key = lock_key
self.expire_ms = expire_ms
self.retry_times = retry_times
self.retry_interval = retry_interval
self.use_exponential_backoff = use_exponential_backoff
self.unique_value = self._generate_unique_value() # 生成唯一标识
self.locked = False # 标记是否已拿到锁
def _generate_unique_value(self) -> str:
"""生成客户端唯一标识(便于排查问题)"""
process_id = os.getpid() # 进程ID
uuid_str = str(uuid.uuid4()) # UUID
return f"{process_id}_{uuid_str}"
def acquire(self) -> bool:
"""获取锁(带重试)"""
retry_count = 0
while retry_count < self.retry_times:
# 执行加锁命令(兼容redis-py 4.x+的写法)
result = self.redis_client.set(
name=self.lock_key,
value=self.unique_value,
nx=True,
px=self.expire_ms
)
if result:
self.locked = True
print(f"获取锁成功(锁key:{self.lock_key},唯一标识:{self.unique_value})")
return True
# 加锁失败,计算重试间隔
retry_count += 1
if retry_count >= self.retry_times:
break
# 指数退避或固定间隔
if self.use_exponential_backoff:
current_interval = min(self.retry_interval * (2 ** (retry_count - 1)), 5)
else:
current_interval = self.retry_interval
print(f"获取锁失败,{current_interval:.2f}秒后重试(剩余次数:{self.retry_times - retry_count})")
time.sleep(current_interval)
print(f"获取锁失败(重试{self.retry_times}次,锁key:{self.lock_key})")
return False
def release(self) -> bool:
if not self.locked:
print("未持有锁,无需释放")
return False
# 解锁Lua脚本(逻辑不变)
unlock_script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
try:
# eval(script, numkeys, *keys_and_args)
# numkeys=1 表示KEYS数组长度为1,后续先传KEYS元素,再传ARGV元素
result = self.redis_client.eval(
unlock_script,
1, # numkeys:KEYS的长度
self.lock_key, # KEYS[1]
self.unique_value # ARGV[1]
)
if result == 1:
self.locked = False
print(f"释放锁成功(锁key:{self.lock_key})")
return True
else:
print(f"释放锁失败:锁已过期或不是当前客户端持有(锁key:{self.lock_key})")
return False
except Exception as e:
print(f"释放锁异常:{e}")
return False
# 优化上下文管理器:获取锁失败时抛出异常,避免进入with块
def __enter__(self):
if not self.acquire():
raise RuntimeError(f"获取锁失败(锁key:{self.lock_key})")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
# ------------------- 使用示例 -------------------
if __name__ == "__main__":
# 初始化Redis客户端
redis_client = redis.Redis(
host="localhost",
port=6379,
db=0,
decode_responses=True,
# 如果Redis有密码,添加password参数
# password="your_redis_password",
# 连接超时设置
socket_timeout=5,
socket_connect_timeout=5
)
# 创建锁实例(开启指数退避重试)
lock = RedisDistributedLock(
redis_client=redis_client,
lock_key="stock:deduct:1001", # 扣减库存的锁
expire_ms=10000,
retry_times=5,
retry_interval=0.2,
use_exponential_backoff=True
)
# 使用with语句(自动加锁/解锁,获取失败会抛异常)
try:
with lock:
print("执行业务逻辑:扣减库存...")
time.sleep(3)
except RuntimeError as e:
print(e)
print("无法执行库存扣减操作")
Redis缓存的业务逻辑设计非常丰富,我来详细解释几种典型的业务场景和实现逻辑。
缓存业务存在的核心问题
缓存业务存在的三个核心问题:
| 问题类型 | 核心定义 | 发生场景 | 核心危害 |
|---|---|---|---|
| 缓存穿透 | 请求缓存和数据库都不存在的 key,请求直接打到数据库 | 恶意攻击(如批量请求不存在的用户 ID)、业务 bug | 数据库被大量无效请求压垮 |
| 缓存击穿 | 热点 key突然过期,大量请求瞬间打到数据库 | 秒杀商品、首页热点数据的缓存过期 | 数据库瞬时压力骤增,可能宕机 |
| 缓存雪崩 | 大量 key同时过期,或 Redis 集群宕机 | 缓存 key 集中设置相同过期时间、Redis 主从切换 / 集群故障 | 数据库被海量请求压垮,系统雪崩 |
一、缓存穿透:防护方案(从易到难)
1. 核心思路
- 拦截无效请求:让不存在的 key "止步于缓存层",不打到数据库;
- 避免空值缓存的内存浪费:设置短期过期时间。
2. 防护方案(按优先级排序)
方案 1:缓存空值(最简单,适用于大部分场景)
- 逻辑 :数据库查询不到数据时,往缓存中写入一个 "空值"(如
None),并设置短期过期时间(如 60 秒); - 优点:实现简单,无需额外组件;
- 缺点:会缓存大量空值,占用少量内存(短期过期可缓解)。
方案 2:布隆过滤器(高效拦截,适用于海量 key 场景)
- 逻辑:提前将所有有效 key 存入布隆过滤器,请求先通过布隆过滤器判断 key 是否存在,不存在则直接返回;
- 优点:空间效率极高,查询速度快;
- 缺点:存在误判率(可接受),不支持删除(需用 Counting Bloom Filter)。
二、缓存击穿:防护方案(针对热点 key)
1.核心思路
- 避免热点 key 过期:要么永不过期,要么过期时 "串行更新";
- 防止大量请求同时打到数据库:用分布式锁控制更新缓存的并发。
2.防护方案(按优先级排序)
方案 1:热点 key 永不过期(最简单,推荐)
- 逻辑:热点 key 不设置过期时间,由后台定时任务主动更新缓存;
- 优点:彻底避免过期导致的击穿;
- 缺点:需维护定时任务,缓存数据可能有短暂不一致(可接受)。
方案 2:互斥锁更新缓存(分布式锁)
- 逻辑:缓存未命中时,只有一个请求能获取锁并更新缓存,其他请求等待或降级;
- 优点:无需修改过期策略,适配所有场景;
- 缺点:增加分布式锁的开销,需处理锁失败的降级逻辑。
方案 3:缓存预热 + 过期时间随机化
- 逻辑:提前加载热点 key 到缓存,且过期时间加随机值(如 3600±600 秒),避免集中过期;
- 优点:辅助方案,降低击穿概率;
- 缺点:无法完全避免(如热点 key 突然被大量访问)。
三、缓存雪崩:防护方案(系统性防护)
核心思路
- 避免大量 key 同时过期:过期时间随机化;
- 提高 Redis 可用性:主从复制、哨兵、集群;
- 降级兜底:Redis 故障时,限制请求流量,保护数据库。
防护方案(组合使用)
方案 1:过期时间随机化(核心)
-
逻辑:给每个 key 的过期时间增加随机值(如 3600±600 秒),避免集中过期;
-
代码示例:
pythonimport random # 设置缓存时,过期时间加随机值 expire_seconds = 3600 + random.randint(-600, 600) redis_client.setex(cache_key, expire_seconds, json.dumps(data))
方案 2:Redis 高可用架构(必须)
- 主从复制:主库写,从库读,主库故障时手动切换;
- 哨兵(Sentinel):自动监控主从状态,故障时自动切换;
- Redis Cluster:分片存储,单节点故障不影响整体服务。
方案 3:多级缓存(本地缓存 + Redis)
- 逻辑:在应用层增加本地缓存(如 Caffeine、Guava Cache),Redis 故障时,优先读取本地缓存;
- 优点:降低 Redis 压力,提高可用性;
- 缺点:本地缓存数据可能不一致(短期可接受)。
方案 4:熔断降级(最终兜底)
- 逻辑:使用 Sentinel/Hystrix 等组件,当 Redis 故障或数据库压力过高时,触发熔断,返回默认值或提示 "服务繁忙";
- 核心指标:QPS、响应时间、错误率。
缓存的基本业务逻辑
一、 读缓存流程(Cache-Aside Pattern)
读缓存的图示逻辑:
是
否
否
是
是
否
否
是
开始:接收查询请求(user_id)
构造缓存key:user:{user_id}
查询Redis缓存
缓存是否命中?
返回缓存中的用户数据
结束
构造分布式锁key:lock:user:{user_id}
尝试获取分布式锁
获取锁是否成功?
直接查询数据库
返回数据库查询结果
双重检查:再次查询Redis缓存
缓存是否命中?
查询数据库
数据库是否存在该用户?
缓存空值(expire=60s)
返回None
释放分布式锁
将用户数据写入Redis(expire=3600s)
返回用户数据
python
def get_user_info(user_id: int) -> dict:
"""获取用户信息(缓存穿透/击穿/雪崩防护)"""
cache_key = f"user:{user_id}"
# 1. 先查缓存
cached_data = redis_client.get(cache_key)
if cached_data:
print(f"缓存命中:{cache_key}")
return json.loads(cached_data)
print(f"缓存未命中:{cache_key}")
# 2. 缓存未命中,查数据库(分布式锁防止缓存击穿)
lock_key = f"lock:user:{user_id}"
try:
with RedisDistributedLock(redis_client, lock_key, expire_ms=3000):
# 2.1 双重检查(Double Check)
cached_data = redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
# 2.2 查询数据库
user_data = db.query("SELECT * FROM users WHERE id = %s", user_id)
if not user_data:
# 缓存空值防止缓存穿透
redis_client.setex(cache_key, 60, json.dumps(None))
return None
# 2.3 写入缓存
redis_client.setex(
cache_key,
3600, # 1小时过期
json.dumps(user_data)
)
return user_data
except RuntimeError:
# 获取锁失败,直接查数据库(降级策略)
print("获取锁失败,降级查数据库")
return db.query("SELECT * FROM users WHERE id = %s", user_id)
二、 写缓存流程
在读写分离架构中,存在主从同步延迟:
text
主库(Master)--同步延迟--> 从库(Slave)
↓写数据 ↓读数据
写缓存流程示例图:
否
是
开始:接收更新请求(user_id, data)
更新数据库(UPDATE users)
数据库更新是否成功?
返回更新失败
结束
构造缓存key:user:{user_id}
立即删除Redis缓存(第一次删除)
启动异步线程执行延迟删除
异步线程:延迟500ms
异步线程:再次删除Redis缓存(第二次删除)
异步线程结束
返回更新成功
python
def update_user_info(user_id: int, data: dict) -> bool:
"""更新用户信息(双写策略)"""
# 1. 更新数据库
success = db.update("UPDATE users SET ... WHERE id = %s", user_id, data)
if not success:
return False
# 2. 更新缓存(延迟双删策略)
cache_key = f"user:{user_id}"
# 2.1 先删除缓存
redis_client.delete(cache_key)
# 2.2 更新数据库(上面已做)
# 2.3 延迟再次删除(异步任务)
def delayed_delete():
time.sleep(0.5) # 延迟500ms
redis_client.delete(cache_key)
threading.Thread(target=delayed_delete, daemon=True).start()
return True