首页 > 数据库 >Redis 缓存/分布式锁/消息队列的应用

Redis 缓存/分布式锁/消息队列的应用

时间:2024-04-30 23:55:23浏览次数:27  
标签:缓存 Redis 数据库 redis 更新 消息 分布式

缓存

缓存是最常见的的应用类型,因为同等配置下,如果一台MySQL能支持上千的QPS,那么一台redis支持的QPS能达到上万,十倍于MySQL。客户端将热点数据存储在redis中,优先从redis读取数据,可以减轻数据库的访问压力。

但将redis作为缓存,也存在一些问题,例如数据不一致。

数据不一致

场景:redis中的缓存数据有更改,需要进行更新。这时候有两种选择:

  1. 先更新redis中的数据,再更新数据库
  2. 先更新数据库,再更新redis

第一种方式速度最快,但是更新redis和更新数据库不是一个原子性操作。可能出现redis更新了,而MySQL更新失败(如无法通过唯一索引约束校验)。这种情况下,redis中的数据就是脏数据了,这是我们无法接受的,所以是通常选择先更新MySQL。

那么先更新MySQL,再更新redis,就能保证数据一致性了吗?

也不一定,常用方案如下:

  1. 先删除缓存,再更新数据库:在更新数据库的过程中,其它请求从MySQL读取数据并设置了缓存,这导致数据库更新后的值与redis中数据不一致。
  2. 先删除缓存,再更新数据库,再删除缓存:在第一种方式的基础上,再加一个更新后再删除一次缓存,这样能一致了吧?也不行。例如请求A在请求B更新数据库前读取了值,由于一些原因阻塞了,请求B更新完了数据库,然后删除了redis缓存。在请求B完成后,请求A被唤醒了,又重新设置了缓存,此时redis的值也和MySQL中不一致。

看来这个数据一致性是真的有点难解决了噢,但再仔细想想,在上述第二种方案的基础上加一个延时是不是就可以了。如下,增加一个延时,延迟时间根据业务实际执行时间来判定,这可以解决很大一部分数据不一致的问题。这种方式叫做​延时双删​​,两次删除操作之间设置了一个延迟。

deleteRedisKey
updateMySQL
# 增加一个延迟,等他其它请求执行完毕
sleep(100)
deleteRedisKey

但再想想,这个延时双删,还要设置一个等待时间,实在不优雅。我们可以借助redis自身的特性来保证数据一致性吗?

个人想法是,可以利用redis的原子性,在更新数据库后,向redis一次性发送两个命令(单线程保证连续执行),包括删除缓存和设置缓存命令,同时设置缓存命令为SETNX,仅在Key不存在时才更新。这样不用担心其它请求将缓存设置为旧版本的数据,自然也就不需要延时了。

缓存数据过期/失效

除了数据不一致问题,将数据缓存在redis中,还存在其它的使用问题。典型的有

  1. 大量请求不走redis,直接访问数据库,例如redis宕机、redis中大量缓存同时失效
  2. 某个热点数据失效,导致大量请求直接访问数据库,例如某个缓存数据过期,导致请求全落在了数据库
  3. 缓存中不存在某个值,导致该请求每次都访问数据库

上述三个问题又分别被称为缓存雪崩、缓存击穿、缓存穿透,也不知道那位高手起的名字,只能说一句佩服佩服。

解决数据失效

前两个问题,可以总结为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

相关文章

  • Redis 高可用之主从架构与哨兵集群
    在redis实例宕机后,通过AOF和RDB可以恢复数据,这是高可用的一部分。但是在宕机期间,如何持续提供服务呢?这是高可用的另一部分。redis的方案是主从库模式,在主库宕机后,由从库提供服务。主从架构遵从单线程处理原则,从库只接受读请求,写请求都在主库执行,主库执行后再同步到从库中去。在......
  • Redis 高可用之主从哨兵集群实战
    搭建集群架构规划为一个主库节点,一个从库节点,三个哨兵节点,其中主从库节点内存配置需保持一致,哨兵节点对配置要求较低,可配置在主从节点上。搭建主库主从库节点内存配置需保持一致,主从库连接密码保持一致。主库不进行任何持久化配置,交给从库完成。编写配置文件需要注意的是,主库......
  • SpringBoot2.x整合Redis Sentinel
    redissentinel搭建之后,在spring-boot项目中集成。配置在pom.xml文件中添加如下依赖配置(这里spring-boot版本2.2.5),这个版本中,默认使用lettuce作为redis连接池。<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis<......
  • Redis 高性能
    为什么Redis性能很高,遥遥领先于MySQL?个人分析有如下原因:IO多路复用物理结构上来说,它是内存数据库,内存的访问速度比硬盘快几个量级。机械硬盘的随机访问速度一般为毫秒级,SSD硬盘的随机访问速度一般为微秒级,而内存的随机访问速度一般为纳秒级。逻辑结构上来说,归功于它的存......
  • Redis 高可用之持久化
    Redis服务实例宕机后,其中的数据还能恢复吗?是的,与其他内存数据库不同(如memcache没有持久化),redis还提供了数据持久化功能,并提供两种持久化方式:AOF(appendonlyfile):逻辑文件,记录的是一条一条的更改命令。在进行数据恢复时,需要一条一条的重放日志,恢复速度较慢RDB(readdatabase)......
  • 微服务:分布式事务
    在分布式系统中,一个服务调用多个远程服务时,多个事务必须同时成功或失败。每一个服务的事务称为分支事务,整个业务称为全局事务 seata架构中有三个角色:TC事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚TM事务管理者:定义全局事务的范围、开始,提交,回滚全局事务RM资......
  • 【Redis】Redis的操作命令(五)——Redis 有序集合(sorted set)
    有序集合添加元素ZADDrunoobkey1redis有序集合移除元素ZRANGErunoobkey010WITHSCORES有序集合命令命令说明例子ZADDkeyscore1member1[score2member2]向有序集合添加一个或多个成员,或者更新已存在成员的分数 ZCARDkey获取有序集合的成员数 ......
  • Unity游戏框架设计之缓存池管理器
    Unity游戏框架设计之缓存池管理器简单介绍在游戏运行的过程中,我们可能遇到这样的需求,即创建大量相同类型的敌人。在传统方法中,我们将对每一个敌人都重新创建,但这样是效率低且占据内存的。为此我们可以这样做,所有敌人在创建时,都从敌人缓存池中取出敌人对象,当敌人死亡时,则将敌人......
  • 同源页面监听缓存改变页面
    onActivated(()=>{this.searchRecordList.value=localStorage.getItem('searchRecord')?JSON.parse(localStorage.getItem('searchRecord')asstring):[];this.getList();window.addEventListener('storage&#......
  • 当Surveymonkey报错Request Header Fields Too Large时需要清理Edge浏览器缓存
     第一步:点击浏览器左上角的...第二步:依次进入Cookie和网站权限---管理和删除Cookie和站点数据。第三步:点击查看所有Cookie和站点数据。第四步:点击全部删除。第五步:清除站点Cooke数据。......