Redis 脚本速查(redis-cli + Python 实测版)
来源:
Django+Redis+MySQL背诵笔记.md(Redis 7.0.15 · redis-py 8.1.0 · django-redis 7.0.0 实测) 口径:Python 3.x;命令与 Python 客户端双线。✅=笔记中实测通过,⚠️=实测踩过的坑。
一、连接
redis-cli
bash
redis-cli -h 127.0.0.1 -p 6379 -n 0 # 连默认实例,-n 选库
redis-cli -h 127.0.0.1 -p 6379 -a '<pwd>' ping # 带密码;生产建议用 REDISCLI_AUTH 环境变量
redis-cli -p 7001 info replication # 看主从状态
Python 直连
python
import redis
r = redis.Redis(
host='127.0.0.1', port=6379, db=0,
decode_responses=True, # 默认返回 bytes,开了返回 str
socket_connect_timeout=5, socket_timeout=5,
retry_on_timeout=True, # ⚠️ 只重试超时,断连不重试
)
r.ping()
Django(django-redis)
python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/0',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'SOCKET_CONNECT_TIMEOUT': 5,
'SOCKET_TIMEOUT': 5,
'CONNECTION_POOL_KWARGS': {'max_connections': 50, 'retry_on_timeout': True},
},
}
}
python
from django.core.cache import cache
from django_redis import get_redis_connection
cache.set('k', {'a': 1}, timeout=60) # ✅ 自动 pickle + key 前缀
cache.get('k') # -> {'a': 1}
cache.add / touch / get_many / incr / get_or_set / delete # ✅ 全部实测通过
r = get_redis_connection('default') # 拿原生客户端
r.set('u:1', 'x', ex=60) # ⚠️ 原生用 ex=,不是 timeout=
⚠️ cache 层与原生客户端的两点差异(实测):
- cache 层 key 被加前缀:逻辑
demo:key实际是:1:demo:key(version 默认 1),值是 pickle 二进制 。原生读必须r.get(cache.make_key('demo:key')),直接r.get('demo:key')返回None。 - 参数名不同:
cache.set(timeout=)✅ vsr.set(timeout=)❌TypeError,原生必须ex=/px=。
二、五种数据类型脚本
String(缓存 / 计数器)
bash
SET k v EX 60 # 秒级过期;PX 毫秒;NX 仅当不存在;XX 仅当存在;KEEPTTL 保留原 TTL
SET k v NX EX 30 # ✅ 分布式锁标准写法(原子)
GET k
MSET a 1 b 2
MGET a b
INCR cnt # ✅ 原子累加
DECR cnt / INCRBY cnt 5 / DECRBY cnt 5 / INCRBYFLOAT price 1.5
SETNX k v # ⚠️ 老写法,配 EXPIRE 非原子,不要用于锁
python
r.set('k', 'v', ex=60)
r.incr('cnt')
r.set('lock:order:1', 'token', nx=True, ex=30) # 抢锁:返回 True/None
Hash(对象 / 字段)
bash
HSET h:1 a 1 b 2
HGET h:1 a / HMGET h:1 a b / HGETALL h:1
HINCRBY h:1 a 1 / HDEL h:1 a / HLEN h:1 / HEXISTS h:1 a
HKEYS h:1 / HVALS h:1 / HSCAN h:1 0 COUNT 100
python
r.hset('h:1', mapping={'a': 1, 'b': 2})
r.hgetall('h:1') # -> {'a': '1', 'b': '2'}
r.hincrby('h:1', 'a', 1)
List(队列 / 栈)
bash
LPUSH q 1 / RPUSH q 2 / LRANGE q 0 -1 / LLEN q
LPOP q / RPOP q / BLPOP q 5 # ✅ 阻塞队列,单位秒
LTRIM q 0 99 # 只留前 100 个
LMOVE src dst LEFT RIGHT # 6.2+ 原子搬运
python
r.lpush('q', 'a'); r.rpush('q', 'b')
r.lrange('q', 0, -1)
r.blpop('q', timeout=5) # 空队列阻塞,超时返回 None
Set(去重 / 关系)
bash
SADD s a b c / SISMEMBER s a / SMEMBERS s / SCARD s
SREM s a / SINTER s1 s2 / SUNION s1 s2 / SDIFF s1 s2
SPOP s / SRANDMEMBER s 3 / SSCAN s 0 COUNT 100
ZSet(排行榜 / 延迟队列)
bash
ZADD rank 100 u1 90 u2
ZINCRBY rank 5 u1 # ✅ 排行榜加分
ZREVRANGE rank 0 9 WITHSCORES # 从高到低取前 10
ZRANGE rank 0 -1 / ZRANK rank u1 / ZCARD rank
ZRANGEBYSCORE rank 60 100
ZADD delay 1735660800 order:1 # 延迟队列:score=执行时间戳
ZRANGEBYSCORE delay 0 1735660800 LIMIT 0 10
TTL / Key
bash
EXPIRE k 60 / PEXPIRE k 60000 / EXPIREAT k 1735660800
TTL k / PTTL k # ✅ -1=永久,-2=不存在
PERSIST k # 去掉过期时间
TYPE k / EXISTS k / DEL k / UNLINK k # ✅ UNLINK 异步删,大 key 用它
SCAN 0 MATCH user:* COUNT 1000 # ✅ 游标遍历,线上不要用 KEYS
三、事务 / 管道 / Lua
WATCH 乐观锁 + MULTI/EXEC
python
with r.pipeline() as pipe:
while True:
try:
pipe.watch('balance') # 监视 key
cur = int(pipe.get('balance') or 0)
pipe.multi() # MULTI
pipe.set('balance', cur - 10)
pipe.execute() # EXEC
break
except redis.WatchError: # ✅ key 被改会抛 WatchError,重试
continue
Pipeline 批处理
python
pipe = r.pipeline(transaction=True) # MULTI/EXEC 包裹
pipe.set('a', 1); pipe.incr('b'); pipe.execute() # ✅ 减少 RTT
分布式锁 + Lua 解锁
python
import uuid
token = uuid.uuid4().hex
ok = r.set('lock:order:1', token, nx=True, ex=30)
# ✅ 抢到返回 True;没抢到返回 None,立即失败不阻塞
UNLOCK_LUA = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
r.eval(UNLOCK_LUA, 1, 'lock:order:1', token) # ✅ 非持有者返回 0,锁还在
四、发布订阅
bash
PUBLISH news "hello"
SUBSCRIBE news
PSUBSCRIBE news.*
python
pubsub = r.pubsub()
pubsub.subscribe('news')
for msg in pubsub.listen():
if msg['type'] == 'message':
print(msg['channel'], msg['data'])
⚠️ Pub/Sub 不持久化、不保证送达;需要可靠消费用 Stream(XADD / XREADGROUP / XACK)。
五、缓存三大坑脚本(实测方案)
穿透:缓存空值
python
val = cache.get(key)
if val is None:
val = db_query(key)
cache.set(key, val if val is not None else '', timeout=60) # ✅ 空值也缓存,TTL 短
return val or None
击穿:互斥锁重建
python
val = cache.get(key)
if val is None:
lock = r.set(f'lock:{key}', '1', nx=True, ex=10)
if lock:
try:
val = db_query(key)
cache.set(key, val, timeout=300)
finally:
r.delete(f'lock:{key}')
else:
time.sleep(0.05)
val = cache.get(key) # 没抢到锁就稍后重读缓存
return val
雪崩:TTL 加随机抖动
python
import random
cache.set(key, val, timeout=300 + random.randint(0, 60)) # ✅ 实测 TTL 329~331 分散
兜底:多级缓存 + 熔断限流 + IGNORE_EXCEPTIONS。
Cache-Aside 一致性
python
update_db()
cache.delete(key) # ✅ 实测:删除后 get 返回 None,下次回源 DB 拿新值
# 更强一致:延迟双删(500ms~1s 后再删一次)或 Canal 订阅 binlog 失效缓存
六、主从 + Sentinel 脚本(实测)
启动 1 主 2 从
bash
redis-server --port 7001 --daemonize yes --save '' \
--repl-diskless-sync yes --repl-diskless-sync-delay 0
redis-cli -p 7002 replicaof 127.0.0.1 7001
redis-cli -p 7003 replicaof 127.0.0.1 7001
⚠️ 不设 --repl-diskless-sync-delay 0 时从库会卡在 state=wait_bgsave;设 0 后 1 秒内同步完成。
Sentinel 配置(sentinel-*.conf)
yaml
port 26379
sentinel monitor mymaster 127.0.0.1 7001 2
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
验证与 failover
bash
redis-cli -p 7001 info replication # master_link_status:up,offset 三方对齐 ✅
redis-cli -p 26379 sentinel masters
redis-cli -p 26379 sentinel replicas mymaster
kill <master-pid> # ✅ 实测 6 秒自动切换,数据完好
# failover 日志:+sdown → +vote-for-leader → +odown #quorum 3/2 → +switch-master
⚠️ Sentinel 会把当前主写回配置文件,重做实验要删掉 sentinel-*.conf 重新生成。 ⚠️ Windows 版 Redis 5.0.14 不支持 fork,主从全量同步永远卡 wait_bgsave,需在 Linux 验证。
redis-py Sentinel
python
from redis.sentinel import Sentinel
s = Sentinel([('127.0.0.1', 27001), ('127.0.0.1', 27002)], socket_timeout=1)
master = s.master_for('mymaster') # 主库客户端
replica = s.replica_for('mymaster') # 从库客户端
s.discover_master('mymaster') # ('127.0.0.1', 7001)
django-redis 7.0 Sentinel 配置
python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://mymaster/0', # ⚠️ hostname 就是 service_name
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.SentinelClient',
'CONNECTION_FACTORY': 'django_redis.pool.SentinelConnectionFactory', # ⚠️ 必须
'SENTINELS': [
('127.0.0.1', 27001),
('127.0.0.1', 27002),
('127.0.0.1', 27003),
],
'SENTINEL_KWARGS': {'socket_timeout': 1},
'SOCKET_CONNECT_TIMEOUT': 2,
'SOCKET_TIMEOUT': 2,
},
}
}
⚠️ 旧写法(LOCATION 传地址列表 + SENTINEL_KWARGS.service_name)在 7.0 报 AttributeError: 'tuple' object has no attribute 'startswith';漏 CONNECTION_FACTORY 报 ImproperlyConfigured: ... CONNECTION_POOL_CLASS is not configured correctly。
七、异常处理与降级
python
from redis.exceptions import ConnectionError as RedisConnErr, TimeoutError as RedisTimeout
def safe_get(key):
try:
return get_redis_connection('default').get(key)
except (RedisConnErr, RedisTimeout) as e:
logger.error('redis down: %s', e)
return None # 降级:回源 DB 或返回默认值
python
# 配置层静默降级
'OPTIONS': {'IGNORE_EXCEPTIONS': True} # ✅ Redis 挂了 set/get/delete 全返回 None,不抛异常
实测结论:
| 项 | 结果 |
|---|---|
宕机时 cache.get/set |
抛 redis.exceptions.ConnectionError,原样透传不包装 ✅ |
| 业务层捕获后 | 可降级回源 MySQL,主流程不崩 ✅ |
| Redis 恢复后 | 连接池自动重建连接,进程无需重启,写入立刻成功 ✅ |
retry_on_timeout=True |
只重试超时,连接断开不自动重试 ⚠️ |
| 重试循环 + 指数退避 | 有效 ✅ |
八、运维脚本
bash
redis-cli info memory / info replication / info stats
redis-cli slowlog get 10
redis-cli config get maxmemory
redis-cli config set maxmemory 2gb
redis-cli config set maxmemory-policy allkeys-lru
redis-cli memory usage key
redis-cli --bigkeys # 扫大 key
redis-cli --hotkeys # 扫热 key(需 LFU 策略)
redis-cli --scan --pattern 'user:*'
redis-cli --latency
SCAN + UNLINK 清理前缀(不要 KEYS + DEL):
python
for key in r.scan_iter(match='tmp:*', count=1000):
r.unlink(key)
⚠️ 实测 5 万 key:KEYS * 52.8ms、SCAN 全量 65.7ms,SCAN 反而更慢。 禁 KEYS 的真正原因是它阻塞单线程,期间其他客户端延迟明显升高。
九、速记
- 过期删除 = 惰性删除(访问时检查)+ 定期抽样(每 100ms 抽 20 个,过期率 >25% 重复抽)。
- 内存淘汰:
maxmemory+maxmemory-policy,纯缓存用allkeys-lru,有持久数据用volatile-lru。 - 「单线程」指命令执行单线程;6.0+ 网络 IO 可多线程。
- 大 key 用
UNLINK异步删;热 key 用本地缓存 + 多副本打散(读多写少场景)。 - Cluster 固定 16384 槽;多 key 操作要用 hash tag
{user}:1保证同槽。