Redis使用
分布式锁
Redission
使用原生Redis设置锁的问题:
- 服务器拿到锁后宕机,锁不能释放,导致阻塞。
设置锁失效时间可以解决上面的问题,但是会导致新的问题: - 设置锁失效时间,在服务器负载过高的时候,会发生锁失效业务还没完成的情况,导致业务代码不互斥。
0信任:不要期待网络服务器按照理想情况运行。
使用Redission自动为锁续命,可以解决上述问题。
String lockKey = req.getBusinessUniqueKey() + "-" + req.getBusinessCode(); |
锁设计原则:谁持有锁,谁才有资格释放锁。
不过,使用这个方案有问题:Redis Master加锁后宕机,新的Master没有同步到加锁的数据,就会存在多把锁。
这时我们需要引入ZooKeeper保持强一致性;或者可以使用RedLock,即集群半数以上机器加锁成功,才是真的加锁成功。
然而,这两种方法都会带来性能开销。
对于难以解决的棘手问题,应该思考如何避免问题发生。
高性能分布式锁:分段锁
针对不同段进行加锁,这样就能允许多把锁存在,从而获得一定并发性能。
基本操作
General
# 返回给定模式的keys |
String
SET key value |
Hash
HSET key field value |
flowchart LR |
List
LPUSH key value1 value2 |
典型场景
栈
当作栈使用
订阅消息
队列,先来后到
- 如微信、微博订阅消息
- 阻塞队列
B<L|R>POP
,队列为空就等待
Set
SADD key mem1 mem2 |
典型场景
抽奖
SADD lottery {user_id} |
点赞、收藏、标签
# 点赞 |
关注、商品筛选
利用集合特性运算
- 如共同关注、推荐关注
- 商品筛选
Sorted Set / ZSet
ZADD key score1 mem1 score2 mem2 |
典型场景
日/月/年热点排行榜
# 记录浏览量 |
Redis for Java
- Jedis (Official Recommand)
- Lettuce
- Spring Data Redis
Redis Data Redis
<!-- Redis --> |
spring: |
|
持久化
RDB
# redis-cli |
AOF
保存执行的命令为日志。每次重启加载所有命令。
# redis.windows.conf |
事务
# 开始事务 |
乐观锁
乐观锁:不认为别人会来抢占资源,所以会直接对数据进行操作,在操作时验证是否资源已被占用。
乐观锁会比较数据是否和原数据一致,一致,说明没有人抢占资源,可以修改。
watch key |
典型场景
对象缓存
SET user:1 <json> |
分布式锁
SETNX product:10003 true ex 10 nx |
计数器
INCR article:readcount:{article_id} |
Web集群共享session
Spring Session + Redis
分布式系统全局序列号
INCRBY order_id 1000 |
Scan 流式遍历
SCAN cursor <KEY RegularExpression> <Count> |
cursor相当于一个下标,每次查询,scan
会给出下一次开始的cursor,由此实现分批查询。
Redis Lua
Lua脚本可以一次性原子地执行多条redis语句。减少网络开销、替代Redis事务。
A Redis script is transactional by defination, so everything your can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
EVAL script numkeys key [key...] arg [arg...] |
numkeys: 键名参数个数
接着可以以1为基址访问keys,如KEYS[1]
$ eval "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 first second |
Jedis使用lua脚本
jedis.set("prod_stock_10016", "15"); |
缓存
缓存穿透
缓存穿透:查询不存在的数据。缓存层、存储层都不命中,造成无效的数据库访问。
缓存的存在是为了控制流量和服务压力、保护后端,防止请求直接查询数据库。
但是查询不存在的数据,或者遭受恶意的缓存穿透攻击,后端服务就容易宕机。
解决方案
- 缓存空对象,即使恶意攻击也不能到达数据库。
- 布隆过滤器:不存在的数据一定不存在。直接丢弃恶意请求。
布隆过滤器是一个大型bit数组和几个不同的无偏(均匀分布的)Hash函数。
- Set:布隆过滤器会使用多个Hash函数(如CRC16、CRC32)对Key进行哈希、取模,得到索引值。每个Hash函数都得到一个slot,然后将这些slot都设为1.
- Get:布隆过滤器会使用同样的哈希计算,如果得出的slot中存在0,那么数据不存在,抛弃。
但是由于使用多个Hash函数,所以即使slot全为1,也不能说明数据一定存在,只是很可能存在。
布隆过滤器的优点是缓存空间占用少,适用于以下场景: - 缓存命中率低
- 数据固定
- 实时性低(数据集大)
Redission布隆过滤器
BloomFilter只有add操作,不能更新数据(可能破坏其他数据slot);想要更新,需要重新初始化。
RBloomFilter<String> bloomFilter = redission.getBloomFilter("name"); |
缓存失效
缓存失效:同一时间大量缓存同时失效,导致数据库压力过大宕机。
解决方案
- 设置随机的过期时间
int expireTime = new Random().nextInt(300) + 300; |
- 设置互斥锁,同一个类型的请求,只访问一次数据库并重建缓存。
缓存雪崩
缓存雪崩:缓存层宕机,所有请求直接访问数据库。
解决方案
- 保证高可用性,Redis集群
- 设置令牌,服务熔断、降级;让用户错峰重试
- 异步削峰,消息队列异步排队处理请求
- 做好冗余和备用方案
缓存双写一致性
两个线程并发更新数据库,由于线程切换,可能导致最终缓存结果与数据库实际数据不一致。
正确的更新方式:
- 更新数据库后,删除缓存。
但是如果多个线程并发读写,写完后没来得及删除缓存,读请求仍然会缓存脏数据。
解决方案
- 牺牲一致性,容忍短时间不一致[1]
- 延迟删除:隔一段时间再删除一次缓存,可以丢到消息队列里异步删除
- 牺牲性能,读写锁串行执行,保证并发读写/写写原子执行
- 适合读多写少场景。Redission读写锁实现如下:
// 读写锁用同一把key |
- 两全,增加复杂度,Cannal监听binlog,同步数据;用这个方案不需要手动更新/删除缓存
- 写多读多场景,不适合用缓存;缓存适合实时性、一致性要求不高的场景。
规范
键值设计
Key:业务:数据库名:id
,如u:{uid}:fr:msg:{mid}
Value:拒绝BigKey,String < 10KB、数据结构元素 < 5k
非字符串BigKey,不要使用del,而是使用scan渐进删除;也要预防过期自动删除(自动删除使用del)造成阻塞。
优化BigKey
容易设计出BigKey的场景:
- 大V的粉丝列表
- 按天存储的统计/用户集合
- 将数据库所有字段Serializable存到Redis
优化思路:
- 拆分成list或hash,分段存储
- 局部读取、删除,而不是对整个BigKey操作
12306余票查询经常与实际库存不一致:查到有余票2,提交订单提示没票 ↩︎