Redis 脚本速查

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 层与原生客户端的两点差异(实测):

  1. cache 层 key 被加前缀:逻辑 demo:key 实际是 :1:demo:key(version 默认 1),值是 pickle 二进制 。原生读必须 r.get(cache.make_key('demo:key')),直接 r.get('demo:key') 返回 None。
  2. 参数名不同:cache.set(timeout=) ✅ vs r.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 保证同槽。
相关推荐
初见月1 小时前
适配器模式详解+对象透传治理
后端
怕浪猫1 小时前
3 小时搭一个能用的聊天机器人后端,我用了这套技术栈
后端·面试·github
xiaoqiMikko2 小时前
Redis 实战(二):宕机会丢多少数据 —— kill -9 与真断电下 RDB / AOF 逐个实测(6.2.4)
redis·后端
李哈哈8522 小时前
微服务依赖冲突
后端
努力的小雨2 小时前
每天盯着屏幕写代码,我终于不想再凑合了
后端
海岳云舟2 小时前
springboot项目中使用AOP
后端
小稀土1232 小时前
5步跑通第一个迭代:敏捷研发管理平台落地最小闭环
后端
zz在掘金337082 小时前
Django 中间件全家桶背诵笔记
后端
斑鸠喳喳2 小时前
调度线程池执行器 ScheduledThreadPoolExecutor
后端