缓存
缓存是最常见的的应用类型,因为同等配置下,如果一台MySQL能支持上千的QPS,那么一台redis支持的QPS能达到上万,十倍于MySQL。客户端将热点数据存储在redis中,优先从redis读取数据,可以减轻数据库的访问压力。
但将redis作为缓存,也存在一些问题,例如数据不一致。
数据不一致
场景:redis中的缓存数据有更改,需要进行更新。这时候有两种选择:
- 先更新redis中的数据,再更新数据库
- 先更新数据库,再更新redis
第一种方式速度最快,但是更新redis和更新数据库不是一个原子性操作。可能出现redis更新了,而MySQL更新失败(如无法通过唯一索引约束校验)。这种情况下,redis中的数据就是脏数据了,这是我们无法接受的,所以是通常选择先更新MySQL。
那么先更新MySQL,再更新redis,就能保证数据一致性了吗?
也不一定,常用方案如下:
- 先删除缓存,再更新数据库:在更新数据库的过程中,其它请求从MySQL读取数据并设置了缓存,这导致数据库更新后的值与redis中数据不一致。
- 先删除缓存,再更新数据库,再删除缓存:在第一种方式的基础上,再加一个更新后再删除一次缓存,这样能一致了吧?也不行。例如请求A在请求B更新数据库前读取了值,由于一些原因阻塞了,请求B更新完了数据库,然后删除了redis缓存。在请求B完成后,请求A被唤醒了,又重新设置了缓存,此时redis的值也和MySQL中不一致。
看来这个数据一致性是真的有点难解决了噢,但再仔细想想,在上述第二种方案的基础上加一个延时是不是就可以了。如下,增加一个延时,延迟时间根据业务实际执行时间来判定,这可以解决很大一部分数据不一致的问题。这种方式叫做延时双删,两次删除操作之间设置了一个延迟。
deleteRedisKey
updateMySQL
# 增加一个延迟,等他其它请求执行完毕
sleep(100)
deleteRedisKey
但再想想,这个延时双删,还要设置一个等待时间,实在不优雅。我们可以借助redis自身的特性来保证数据一致性吗?
个人想法是,可以利用redis的原子性,在更新数据库后,向redis一次性发送两个命令(单线程保证连续执行),包括删除缓存和设置缓存命令,同时设置缓存命令为SETNX,仅在Key不存在时才更新。这样不用担心其它请求将缓存设置为旧版本的数据,自然也就不需要延时了。
缓存数据过期/失效
除了数据不一致问题,将数据缓存在redis中,还存在其它的使用问题。典型的有
- 大量请求不走redis,直接访问数据库,例如redis宕机、redis中大量缓存同时失效
- 某个热点数据失效,导致大量请求直接访问数据库,例如某个缓存数据过期,导致请求全落在了数据库
- 缓存中不存在某个值,导致该请求每次都访问数据库
上述三个问题又分别被称为缓存雪崩、缓存击穿、缓存穿透,也不知道那位高手起的名字,只能说一句佩服佩服。
解决数据失效
前两个问题,可以总结为redis服务不可用而导致的数据库压力猛增,一些参考方案为:
- 优化缓存使用,调整热点数据的缓存过期时间,适当增加存活时间,或者设为不过期,手动更新。
- 优化redis架构,搭建高可用的架构,避免服务不可用,如哨兵、集群架构
- 优化数据库,如增加buffer pool内存,让更多的数据页留在内存中,可以猛猛增加QPS
- 优化服务端,增加熔断、降级配置
解决数据不存在
针对第三个缓存不存在的情况,可以利用redis中提供布隆过滤器,利用数据库中已有值创建一个布隆过滤器。在处理请求时,先通过布隆过滤器判断是否存在相关数据,若不存在,直接返回。
为什么会存在这种情况,值都没有,怎么还猛猛请求?
可能某个热点值,不小心被人从数据库删除了,而业务端还在使用。
特殊缓存
用做缓存时,除了缓存一些简单的键值对信息,还可以有一些高级应用。
例如,redis中提供了一个叫做布隆过滤器(Bitmap)的数据类型,通过布隆过滤器,可以先判断值是否存在,避免在值不存在的情况下将请求压力由redis传递至底层数据库。
redis中还有一个ZSet类型,常用作排行榜、任务队列(可作为延时队列实现),实时热点数据统计等等。
redis中还有一个GEO类型,可以缓存数据的坐标点经纬度信息,可以使用其查询某个数据几公里范围内的数据信息,例如根据用户当前位置,查询周围外卖店家时。
分布式锁
在服务进行多实例部署时,单纯在代码中加锁已经无法控制并发了,这个时候需要一个中间件来提供分布式锁服务,而redis就可以作为这个中间件。
基础实现
向redis中新增一个分布式锁的Key,如果新增成功则代表持有这个分布式锁。其它请求必须等待这个Key的释放之后,通过新增这个Key来获得分布式锁之后,才能继续执行业务程序,这样就实现了一个分布式锁。
存在的问题
这种方式实现的分布式锁其实是极不可靠的,没有考虑到redis自身特性。实现分布式锁的基础要求是数据的强一致性,而redis本身是AP实现,不管是单机、哨兵、集群架构,都不能保证数据的强一致性。
在默认情况下,redis的持久化为RDB,隔一段时间做一次持久化操作,如果在两次持久化操作期间redis宕机了,那么在此期间的数据就全部丢失了,无法保证分布式锁Key的一致性。
那么将持久化方式改为AOF+RDB,并且AOF刷新策略为always的话,每次更改都同步落盘时,可以保证唯一性吗
- 对于单机模式的redis来说,的确可以保证唯一性,但是QPS会下降很多,有些得不偿失。并且单机无法保证高可用,宕机后无法获取分布式锁,直接影响业务。
- 对于哨兵、集群模式的redis来说,即便是同步落盘也不能保证数据的一致性。因为主从同步是异步的,在主库写入后,如果主库宕机了,从库被选举为新主库,这时候新主库中是没有该分布式Key数据的。
由此可见,仅通过在一个redis节点中设置一个Key的方式来实现分布式锁是不可靠的。
分布式锁算法
为了解决单节点加分布式锁存在的不可靠问题,Redis开发者提出了分布式锁算法RedLock。
RedLock算法的基本逻辑是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,就可以认为客户端成功地获得分布式锁了;否则加锁失败。
这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
在Redission框架中,其中提供了分布式锁算法RedLock的实现,还提供锁的自动续期等功能。
这种方式需要多个主库节点,只适用于集群架构的redis。极端情况下,集群中全部主库节点同时宕机后重启,也会丢失分布式锁。但真发生这种情况,谁还能有办法呢?
表锁
在没有redis集群时,通过数据库其实也是可以实现分布式锁的。
创建一个表来存储锁信息,包括锁名称、持有锁的客户端信息、锁的超时时间等字段。
CREATE TABLE distributed_lock (
lock_name VARCHAR(50) PRIMARY KEY,
client_id VARCHAR(50),
expiration_time TIMESTAMP
);
消息队列
Streams
在redis5.0中,新增了一个数据类型Streams,专门用于消息队列的实现。Streams可以保证消息的有序性、避免消息的重复消费和服务端的可靠消费,与Streams配套的还有一系列的操作命令
- XADD:插入消息,可以自动生成全局唯一ID
- XREAD:读取消息
- XREADGROUP:按消费组形式读取消息
- XPENDING:查询每个消费组内所有消费者已读取但尚未确认的消息
- XACK:确认消息处理已完成
有序性
redis是单线程操作,按照写入顺序进行保存,可以保证在单节点中有序。但在redis cluster中不能保证整体有序,因为数据分散在多个节点上,新增消息时根据Key的哈希计算决定分布在那个节点。
避免重复消费
可以为每条消息生成一个分布式Id,服务端读取消息处理成功后,将该Id缓存起来。后续先判断该Id是否已经消费,如果已经消费则丢弃。
可靠消费
当客户端端读取消息后,还没执行业务逻辑就宕机了,那这条消息岂不是相当于没消费?是的,但对于这种情况,redis中也有解决方案。Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,消费者读取消息并且消费成功后,需要向redis发送一个确认。如果redis没收到这个确认,消息就会留存在pending list中。后续,服务端可以获取pending list中的消息,进行再次消费。
缺陷
数据丢失
使用redis作为消息队列时,需要知道redis数据保存在内存中,处理速度很快,但在宕机时,可能会丢失还没有消费的消息。如果你能接受几分钟内的消息丢失,那么OK,使用redis吧,速度嘎嘎快。
重复消费todo 待测试
在主从架构中,主库执行读写请求,从库执行读请求。在多个客户端并发读取Stream中的数据时,会导致读取到重复的消息。
解决方法为:
- 多个客户端间使用分布式锁,确保只有一个客户端进行消费
- 接口幂等,读取到消息时,先判断该消息是否消费过,若已经消费过则直接放弃。在读取到消息时,先将消息Id缓存在redis中。
- 使用consumer group的方式进行消费,会记录消费进度,避免重复消费。但是要确保只在主库上执行,如果在主从库上同时读取,还是会。
应用场景
我能想到的典型应用场景是在日志收集时,替换Kafka集群。部署一个kafka集群还得部署一个zookeeper集群,使用更多服务器资源,部署、运维也麻烦。正好日志数据一致性要求也没那么高,用redis也能满足要求。
由Filebeat收集服务端日志后,输出到Redis中,然后由Logstash从Redis中读取日志进行消费,经过一些处理后,输出到elasticsearch集群。
标签:缓存,Redis,数据库,redis,更新,消息,分布式 From: https://www.cnblogs.com/cd-along/p/18168911