Redis缓存
一、缓存的使用
为了提升系统性能,我们一般都会将部分数据放入缓存,加速访问。
适合放入缓存的数据:
- 即时性、数据一致性要求不高的数据(如分类信息、Cookie)
- 访问量大且更新频率不高的数据(读多写少)
举例:电商类应用,商品分类、商品列表、物流状态信息等适合加缓存并加一个失效时间
二、缓存失效问题(缓存穿透、击穿、雪崩)
1. 缓存穿透
描述:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
风险:利用不存在的数据进行攻击,数据库压力瞬间增大,最终导致崩溃。
解决:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
null结果缓存,并加入短暂过期时间。这样可以防止攻击用户反复用同一个id暴力攻击;
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2. 缓存击穿
描述:缓存击穿是指缓存中的一些热点Key数据失效,这时由于瞬时并发特别高,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
风险:数据库压力过大甚至down机。
解决:
- 设置热点数据永远不过期;
- 加互斥锁:大量并发只让一个请求去查询数据库,其他请求等待,查到数据放入缓存后释放锁,其他人获取锁后先查缓存,缓存就会有数据,不用去查DB; 最好能根据Key加锁。
加锁流程: 查询缓存 -> 加锁(设置过期时间) -> 查询缓存 -> 查询数据库 -> 放入缓存 -> 释放锁 -> 返回结果
3.缓存雪崩
描述:缓存雪崩是指缓存中大批量数据设置相同的过期时间,导致缓存在每一时刻同时失效,请求全部转到DB,引起数据库压力过大甚至down机。
和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
风险:数据库压力过大甚至down机。
解决:
- 缓存数据的过期时间基础上增加一个随机时间,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据(频繁使用的数据)永远不过期。
三、缓存数据一致性问题
1.双写模式
定义:数据库写完数据(新增或修改)、写缓存。
问题:并发修改的时候可能会出现暂时性的脏数据问题,但是在数据稳定、缓存过期之后又可以得到最新的正确数据。双写模式只能保证数据最终一致性。
例子:写数据库(线程1) -> 写数据库(线程2) -> 写缓存(线程2) -> 写缓存(线程1)
解决:加锁(通常加读写锁),写数据库和写缓存整个流程加上锁。并且所有缓存有过期时间。
2.失效模式
定义:数据库写完数据(新增或修改)、删除缓存中的数据。
问题:并发修改的情况下也会出现脏数据问题
解决:加锁(通常加读写锁)
3.解决缓存一致性问题
无论是双写模式还是失效模式,高并发情况下都有可能导致缓存的不一致问题。
我们应该通过系统设计来解决。
- 放入缓存的数据不应该是实时性、一致性要求很高的数据,所以给缓存加上过期时间,保证最一致性就行。
- 实时性、一致性要求高的数据,可以直接查数据库。实在要求放缓存可以采用canal订阅binlog的方式。
采用canal订阅binlog的方式可以完美解决缓存不一致问题,但会增加系统设计的复杂性。
四、缓存工具
本地缓存:Caffeine 、 Guava Cache
分布式缓存:redis、memcached
五、分布式锁
常见实现方案: zookeeper
、redis
1、zookeeper
大致思想: 每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
优点:
- 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题
- 实现较为简单
缺点:
- 性能上不如使用缓存实现的分布式锁,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能
- 需要对Zookeeper的原理有所了解
排他锁
定义锁
:通过Zookeeper上的数据节点来表示一个锁。获取锁
:客户端通过调用create
方法创建表示锁的临时节点,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况。释放锁
:以下两种情况都可以让锁释放:1. 当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除; 2. 正常执行完业务逻辑,客户端主动删除自己创建的临时节点;
共享锁
定义锁
:通过Zookeeper上的数据节点来表示一个锁,是一个类似于/lockpath/[hostname]-请求类型-序号
的临时顺序节点获取锁
:客户端通过调用create
方法创建表示锁的临时顺序节点,如果是读请求,则创建/lockpath/[hostname]-R-序号
节点,如果是写请求则创建/lockpath/[hostname]-W-序号
节点判断读写顺序
:大概分为4个步骤:创建完节点后,获取
/lockpath
节点下的所有子节点,并对该节点注册子节点变更的Watcher监听确定自己的节点序号在所有子节点中的顺序
对于读请求:1. 如果没有比自己序号更小的子节点,或者比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑 2. 如果有比自己序号小的子节点有写请求,那么等待 。
对于写请求:如果自己不是序号最小的节点,那么等待。
接收到Watcher通知后,重复步骤1)
释放锁
:与排他锁逻辑一致
2、redis
redis
分布式锁一般使用高级封装框架Redisson
redis官方文档: https://redis.io/topics/distlock
1、加锁:使用SETNX
原子性加锁,设置过期时间、并为每个竞争锁的节点加上唯一标识(UUID、token)
2、解锁:使用redis官方提供的lua脚本原子性解锁
基于缓存实现分布式锁总结
优点:
- 性能好
缺点:
- 实现中需要考虑的因素太多
- 通过超时时间来控制锁的失效时间并不是十分的靠谱