redis的使用场景及涉及到的一些问题
缓存
也不知道下面三个概念一开始是谁提出来的,归类方式很不科学…
缓存穿透
请求了redis和DB中不存在的数据(无效请求),导致请求redis直接访问DB
方案
- 将无效返回值存入redis,使无效请求不会访问DB
- 在应用层拦截请求,例如逻辑校验、布隆过滤器…
缓存雪崩
集中创建redis缓存,导致缓存集中失效,大量访问导致DB周期性压力
方案
- 在预设定的ttl上添加一个随机值,打散key的失效时刻
缓存击穿
热点数据(热key)失效时刻导致DB巨大压力
方案
- 设置缓存不要失效
- 锁/队列
队列
使用list类型的LPUSH+BRPOPLPUSH+LREM可以构成一个简单的消息队列,相比lpush+rpop的实现多了ack机制
简单流程:
- producer使用LPUSH发送消息(send)
- consumer使用BRPOPLPUSH接收消息(consume)
- consumer处理完消息后使用LREM确认消息(acknowledge)
LPUSH queue message
BRPOPLPUSH queue process_queue
LREM process_queue -1 message
其中涉及几个细节:
- 调用BRPOPLPUSH时,如果一直阻塞可能导致redis主动断开连接,抛异常。需要做个重试逻辑
- 如果根据key做了分片,需要要保证两个list在同一分片
延时队列
使用zset类型的ZADD+ZREVRANGEBYSCORE+ZREM可以构成一个简单的延时队列。生产者将需要处理消息时刻作为zset中的score,消费者则轮询zset中score大于now的消息进行处理
分布式锁
可重入性
如果不关注锁的可重入性,那么加锁逻辑很简单:
SET key_name constant_value NX PX ttl
如果需要可重入性的话,为了原子性必须借助lua,以下给出Spring提供的加锁lua
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
elseif not lockClientId then
redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
return true
end
return false
Spring在锁逻辑外用ConcurrentHashMap加了一层缓存。即先在单机竞争锁,竞争到了再去分布式环境竞争锁。
Redlock
先前提到的都是单节点方案,而redlock是一种分布式锁的方案。在有效期内超过半数的实例获得锁则算作获得锁…
与zookeeper的实现相比,redis更注重CAP中的AP,zookeeper更注重CP。使用redis实现分布式锁的优点在于成本更低。如果对于一致性有高要求的话(主从/多IDC)个人觉得不如选择zookeeper。
关于redis分布式锁可见:https://redis.io/topics/distlock