Redis简单介绍

Redis是基于内存的高性能key-value存储(支持多种数据结构:string、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Stream等)

内存读写极快,支持持久化(RDB/AOF)、单线程处理命令(命令执行是原子性的)、支持复制、哨兵高可用和Cluster分片

常用场景:缓存、会话、限流、计数器、排行榜、消息队列、分布式锁等

持久化:RDB(快照):低开销,可能丢数据点(适合备份);AOF(命令追加):更耐久但文件更大、更慢些(可配置fsync策略)

内存管理/驱逐策略:

  • maxmemory用来限制内存;当满时按maxmemory-policy决定如何驱逐:noevictionallkeys-lruvolatile-lruvolatile-ttlallkeys-random等,选allkeys-lru常用于缓存场景

原子性/事务/脚本:单命令原子:MULTI/EXEC是事务队列(不保证隔离);Lua脚本在Redis内部执行,整体原子且性能好(常用于实现安全释放锁等)

集群:RedisCluster按16384个hash slots分片,跨分片的多键操作有限制(需要在同一slot才能多键操作)

监控与调优:关注INFOslowlog、内存使用、evictions、命中率(hit ratio)、客户端阻塞等

分布式锁

为什么需要分布式锁

当多个进程/机器并发的访问同一共享资源时,需要分布式锁保证互斥

分布式锁的必要属性

  • 互斥性:同一时刻只有一个持有者
  • 安全释放:只有锁的持有者能释放该锁
  • 可用性/死锁防止:如果持有者崩溃,锁应当在TTL后自动释放,或可被续租
  • 性能:加/释放开销要小

用Redis实现分布式锁

获取锁(核心命令)

1
SET key value NX PX <ttl>
  • NX:只在key不存在时设置,相当于SETNX
  • PX <ttl>:设置过期时间,毫秒
  • value:必须是唯一标识,例如UUID,用来标记谁是锁的拥有者

伪代码示例:

1
2
3
4
5
6
clientId=UUID()
ok==redis.set("lock:order:123",clientId,NX,PX,10000)
if ok == "OK":
//获取资源
else:
//失败,稍后重试或返回错误

安全释放(绝对不要用DEL key直接释放)

必须先检查自己是持有者(vlaue匹配)再删除,最安全的方法用Lua脚本在Redis端原子执行:

1
2
3
4
5
6
-- release.lua
if redis.call("GET",KEYS[1])==ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end

在客户端:EVALEVALSHA执行上述脚本,传参KEYS[1]=lock_key,ARGV[1]=clientId

重试与超时策略(获取锁)

  • 避免忙等(spin);使用指数退避(exponential backoff)或固定短间隔重试直到总超时时间耗尽
  • 设计合理的lock TTL:ttl=expected_work_time+safety_margin不要设得太短(可能任务还没做完锁就过期),也不要太长(若持有者挂了,会长时间阻塞别人)

端续租(续期)

如果任务可能比最初预计长,需要一个续租/自动刷新机制:

  • 客户端在锁到期前周期性延长TTL(但要确保只有持有者能延长,检查value)
  • 风险:客户端暂停/GC导致错续,需谨慎;续期一定要有失败回退逻辑

示例伪代码,获取+释放

1
2
3
4
5
6
7
8
9
10
function tryLock(key,clientId,ttl,waitTimeout):
endTime=now()+waitTimeout
while now()<endTime:
if redis.SET(key,clientId,NX,PX,ttl)==OK:
return true
sleep(small_backoff)
return false

function releaseLock(key,clientId):
-- call Lua script shown above

Redlock(多节点Redis)和争议

  • Redlock是Antirez(Redis作者)提出的一个算法:在多个独立Redis节点上尝试拿锁,超过半数即认为获得锁,解锁从所有节点删除。目的是提高可用性/容错

  • 争议:一些研究/文章认为Redlock在网络分区下并不能完全实现强一致性(特别是时钟漂移、网络延迟等)。因此在强一致性要求极高的场景,推荐使用基于一致性算法(Paxos/Raft)的系统(etcd、Zookeeper、Consul)来做分布式锁

  • 工程实践:若场景读写频繁且对性能要求极高,Redis-based lock(单实例或哨兵保证高可用)足够;若需要严格一致性,优先选择etcd/sookeeper

注意事项

  • 不要把锁用于长时间操作,锁持有时间应短
  • 锁的TTL要比操作预期时间长,但不能过长:若短可能导致自动释放后另一个客户端获得锁并同时有前一个客户端继续执行(重复执行问题)
  • 释放锁时必须检查owner(Lua)
  • 不要在锁中做大量I/O或阻塞操作(会增加失败概率)
  • 考虑时钟与网络延迟:不要假设绝对精确的时钟或零延迟

缓存

常见缓存模式

  • Cache-Aside(旁路缓存/应用控制缓存):流程:应用先查缓存->缓存未命中则查数据库->将结果写入缓存->返回.写操作:先写DB->删除/更新缓存(或同时更新)。优点:灵活,常用。缺点:需要处理并发写入/缓存失效引发的不一致

  • Read-Through/Write-Through/Write-Behind:

    • Read-Through:缓存自己去加载DB(缓存库/中间件实现)
    • Write-through:写缓存同时写DB(同步)
    • Write-behind:写缓存先返回,由后台把缓存写入DB(异步)。这些模式适合不同一致性/性能权衡。
  • Stale-While-Revalidate(容忍旧数据,后台刷新),缓存过期时仍返回旧值给用户,同时后台刷新缓存(减少击穿)

缓存失效问题

  • 缓存穿透:请求大量不存在的数据(或恶意请求),每次都会落到DB,绕过缓存,造成DB压力

  • 缓存击穿/缓存穿透(有些文献称”击穿”为热点key失效导致的高并发打到DB),指某个(热点)key在某一瞬间过期,大量并发请求同时去DB重建缓存,DB瞬时被打垮

  • 缓存雪崩(cache avalanche)
    多个key在同一时间过期/缓存服务宕机/网络波动导致大面积缓存失效,从而大量请求直接打DB,引发连锁故障

各类问题的解决方法

缓存穿透的常见策略

  • 参数校验:对请求参数(ID格式、范围)做校验,直接拒掉明显非法请求
  • 布隆过滤器(Bloom filter):把所有合法key(或大概率合法)放入Bloom Filter,查询前先校验(Bloom有误判但不漏报,可能拒绝掉一些合法键,但不会把不存在的当存在)。优点内存占用小,查询快;但需要维护(新增/删除)和误判处理
  • 缓存空结果:对于DB确认不存在的key,缓存一个空对象(或特殊值)但TTL设短(比如1-5分钟),避免短时间内重复打DB。注意防止空值被缓存太长造成的错漏。
  • 限流/反爬虫:对频繁访问不存在key的请求进行限流或封禁IP

缓存击穿(单个key失效)解决

  • 互斥锁:当缓存miss时,只有一个请求去DB获取并重建缓存,其他请求等待/返回旧值或轮询重试。实现方法:

    • 应用内singlefight(Go的singlefight包)——在单机实例内合并并发请求。
    • 分布式情况下,用Redis的锁(SET NX)让单个实例去重建缓存
  • 永不过期/极长TTL+后台异步刷新:把热点数据设置长TTL,并用后台任务周期刷新(适用于热点且变化不频繁的数据)

  • Stale-while-revalidate:返回过期前的旧值并异步刷新(对用户体验友好)

伪代码(互斥锁重建)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
value=redis.get(key)
if value!=nil:
return value

if acquire_lock("lock:"+key):
//double check,别重复查库
value = redis.get(key)
if value !=nil:
release_lock(...)
return value
value=db.get(key)
redis.set(key,value,ttl)
release_lock(...)
return value
else:
//其他请求等待或短轮询,或直接返回适当错误
sleep(short)
goto try again

缓存雪崩(大面积失效)解决

  • TTL随机化:给不同key设置随机的TTL(例如base_ttl * (0.8~1.2)),避免同一时刻大量过期
  • 缓存预热(Warm-up):上线/重启/版本切换后,主动把热点数据预先填入缓存。
  • 多级缓存:第一层内存缓存(应用进程本地),第二层Redis,减少瞬时Redis压力。若Redis宕机,第一层还能缓解短时间内压力。
  • 降级与限流:流量激增时,服务降级(返回默认值/静态页面),或对DB进行限流保护。
  • 扩容/冗余:保证Redis集群有足够容量和高可用部署,避免单点故障。

实用示例

安全释放锁(必须用)

1
2
3
4
5
6
-- release.Lua
if redis.call("GET",KEYS[1])==ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end

使用(伪)

1
EVALSHA <sha1> 1 lock:key <clientId>

获取锁带重试(伪Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String lockKey = "lock:order:123";
String clientId = UUID.randomUUID().toString();
long ttl=10000;//ms
long waitTimeout=5000;//ms

long end=System.currentTimeMillis()+waitTimeout;
while(System.currentTimeMillis()<end){
String ok=jedis.set(lockKey,clientId,"NX","PX",ttl);
if("OK".equals(ok)){
//got Lock
break;
}
Thread.sleep(50);//backoff
}

释放用上面的Lua脚本(执行EVAL)

缓存旁路(伪代码)

1
2
3
4
5
6
7
8
9
10
11
12
function getUser(id):
key = "user:"+id
value = redis.get(key)
if value != null:
return value
//miss
user = do.query(id)
if user == null:
redis.set(key,NULL_PLACEHOLDER,short_ttl)
return null
redis.set(key,serialize(user),normal_ttl)
return user

最佳实践清单

  • 缓存:

    • 使用cache-aside模式常见且灵活
    • 对不存在的值缓存短期空值,避免穿透
    • 热点key使用长TTL+异步刷新或stale-while-revalidate
    • 给TTL添加随机抖动,避免同化失效
    • 监控命中率、evictions、内存、慢查询
    • 锁value必须唯一(UUID);释放时要校验value再DEL(Lua)
    • 锁TTL选取合理;若需要更长,做安全续租
    • 避免在锁中做长时间阻塞或外部调用
    • 对强一致性需求高的场景,考虑etcd/zookeeper
  • Redis选型/运维

    • 选择合适持久化(RDB/AOF)与持久化策略
    • 生产环境用Cluster/哨兵/Replica做高可用与扩容
    • 设置maxmemory和合适eviction policy
    • 定期运行内存分析,避免大value(如把大对象拆成hash)

常见问答

Redis锁能保证绝对安全吗?

单实例+SET NX PX +Lua脚本能解决大部分问题,但在网络分区、客户端挂起(GC)或多实例复杂失败场景下仍有edge-case。需要严格一致性时选用Raft/Paxos系统(etcd/zookeeper)

布隆过滤器会占用内存吗?误判怎么办?

布隆滤器用很少表示海量key,误判率可通过位数组大小和hash函数数目调节。误判会把某些合法key误判为”可能存在”,需设计容错(短TTL)

Redis单线程是不是性能瓶颈?

Redis单线程是命令处理单线程,I/O使用multiplexing,单命令执行速度极快。瓶颈通常在网络、内存或慢命令(如复杂的KEYS)上