Loading

使用Redis必须考虑的问题

总结:

  1. Redis有哪些数据类型?
    string,list,hash,set,zset,位图,HyperLogLog,布隆过滤器。

  2. Redis的过期策略?
    定期删除+惰性删除,内存淘汰策略。

  3. Redis如何持久化?
    RDB+AOF。

  4. 如何保证Redis的高可用和高并发?
    主从复制(快照同步+增量同步),哨兵,集群。

  5. 如何保证缓存和数据库的数据一致性?
    简单双删策略,延迟双删策略,串行化策略。

  6. Redis为什么那么快?
    NIO网络交互+单线程+内存级别操作。

1 为什么使用缓存?

高性能、高并发。

缓存主要是用来提高获取数据的速度,通过将一些热点数据存储在缓存中,可以大大提高业务处理的速度,因此可以提高系统的性能和并发能力。

在实际业务场景中,也可以用来缓存一些特殊数据,例如登录用户的token、分布式锁等。

2 Redis有哪些数据类型?

string,list,hash,set,zset,位图,HyperLogLog,布隆过滤器。

string可以存储字符串:

set name Xianhuii

list可以存储有序列表:

rpush books Java JavaScript Spring

hash可以存储多个键值对:

hset person name Xianhuii
hset person age 18

set可以存储去重的无序列表:

sadd books python
sadd books python

zset可以存储去重的排序列表:

zadd books 10 Java
zadd books 8 Spring
zadd books 9 Java

位图可以按位存储信息:

setbit bitMap 1 1
setbit bitMap 2 0

HyperLogLog可以用于统计大量数据:

pfadd users Xianhuii
pfadd users CHUAN
……
pfcount users

布隆过滤器可以用于大量数据的过滤:

bf.add users Xianhuii
……
bf.exists user CHUAN

3 Redis的过期策略?

定期删除+惰性删除,内存淘汰策略。

Redis中的key都可以设置过期时间,对于这类数据,会采用定期删除+惰性删除策略:

  • 定期删除:Redis将设置过期时间的key存储在一个独立字典中,会定时扫描这个字典,根据过期时间来判断是否删除该key。
  • 惰性删除:如果某个key过期了,但是还没进行定期删除扫描,此时客户端获取这个过期key时,Redis会先判断过期时间,如果过期了就立即删除。

定期删除+惰性删除策略在日常工作中也有很多应用,它的原理就是使用定时任务进行轮询,保证整体业务的稳定性;对于轮询间隔的少量数据,使用及时处理的兜底策略。

Redis中的key也可以不设置过期时间,这部分数据不能用定期删除+惰性删除策略进行处理。如果内存中存在大量未设置过期时间的key,以及大量设置过期时间但未过期的key,造成内存达到了上线,此时定期删除+惰性删除策略就派不上用场了。

此时,Redis会采取内存淘汰策略:

  • noeviction(默认):不再处理写请求,可以继续执行读请求和删除请求。
  • volatile-lru:优先淘汰最少使用的、设置过期时间的key。
  • volatile-ttl:优先淘汰ttl小的、设置过期时间的key。
  • volatile-random:随机淘汰设置过期时间的key。
  • allkeys-lru(建议使用):优先淘汰最少使用的全体key。
  • allkeys-random:随机淘汰全体key。

简单来说,淘汰策略:

  • 不再写入,需要手动删除。
  • 设置过期时间的key集合/全体key集合中进行最少使用淘汰/最小ttl淘汰/随机淘汰

4 Redis如何持久化?

RDB,AOF。

Redis的数据全部存储在内存中,如果宕机会造成数据丢失。为此,Redis提供了持久化功能:

  • RDB:定时(周期较长)将内存中的全部数据保存到磁盘文件中,每次都会新生成该时刻的数据文件。
  • AOF:记录对内存进行修改的指令记录,由于可能会有覆盖操作,需要定时对该日志进行瘦身重写,即定时将内存数据转换成操作指令。

RDB是对内存数据的整体备份,生成的文件通常比较少,恢复速度快。但是由于备份周期长,可能会丢失一部分未及时备份的数据。

AOF是对操作指令的记录,随着运行时间增长会变得越来越大,恢复速度慢。

实际项目中通常使用RDB+AOF混合持久化策略,使用RDB定期备份内存中的数据,使用AOF记录上个RDB备份周期至今的操作指令。由此既保证了数据的完整性,又大大减小了AOF指令文件,加快了恢复速度。

5 如何保证Redis的高可用和高并发?

主从复制,哨兵,集群。

Redis高可用,简单来说就是要保证某个服务器宕机后不能影响客户端的业务,Redis提供了主从复制+哨兵功能。

Redis高并发,简单来说就是要能够快速处理客户端的请求,所以通常需要增加服务器实例的数量,Redis提供了集群功能。

实际上主从复制哨兵集群是不可分的,它们往往会结合到一起工作。

5.1 主从复制

快照同步,增量同步。

可以为Redis服务器设置一个master节点和多个slaver节点,master负责与客户端进行交互,slaver则会使用快照同步+增量同步的方式,定时从master(主从同步)或slaver(从从同步)同步数据。

快照同步类似于RDB持久化方式,它会将内存中的数据持久化到磁盘文件,然后将该磁盘文件数据同步给slaver。

增量同步类似于AOF持久化方式,它会将修改操作指令记录到缓存中,然后将该缓存数据同步给slaver。

5.2 哨兵Sentinel

哨兵是Redis主从节点的管理器,它会持续监控主从节点,当master宕机时,会选择最优的slaver作为新的master。

使用哨兵的流程:

  1. 客户端连接哨兵,请求master地址。
  2. 客户端连接master进行操作。

如果master宕机重新选取后,客户端需要重新上述步骤,获取最新的master地址。

5.3 Redis Cluster

Redis Cluster是官方的集群化方案,它本身提供类似主从复制和哨兵的功能。

Redis Cluster是去中心化的,它将所有数据划分为16384个槽位,每个节点负责一部分槽位,保存一部分数据。当客户端连接集群时,会返回一份集群的槽位配置信息。客户端需要查找某个key时,可以直接定位到目标节点。

Redis Cluster可以为每个主节点设置若干从节点,当从节点发生故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么宕机时就会造成整个集群不可用。

Redis Cluster通过Gossip协议广播来保证集群中实例间的信息同步。如果某个节点发现节点A失联了(PFail,Possible Fail),会向整个集群广播。各个节点会判断节点A的连接情况,并广播判断结果。如果集群中大部分节点都判断该节点失联,就会将节点A下线(Fail),并且进行主从切换。

6 如何保证缓存和数据库的数据一致性?

简单双删策略,延迟双删策略,串行化策略。

在使用Reids缓存数据库数据时,就会出现缓存数据和数据库数据不一致问题。

根据对数据一致性的不同程度要求,我们可以采取递进的措施。

6.1 简单双删策略

如果对数据一致性要求不严格,可以使用以下简单的双删策略:

读取数据:

public Result read() {
	// 读取缓存
	// 如果缓存没有,读取数据库
	// 更新缓存
	return res;
}

更新数据:

public void update() {
	// 删除缓存
	// 更新数据库
	// 删除缓存
}

正常情况下,updateread操作会按顺序执行。每次update操作都会更新数据库数据,删除缓存。每次read操作都会获取数据库中的最新数据作为缓存。

但是在实际线上环境中,readupdate可能会并发执行,在小概率情况下可能会按以下顺序执行,造成数据不一致:

  1. read读取缓存
  2. 缓存没有,read读取数据库
  3. update删除缓存
  4. update更新数据库
  5. update删除缓存
  6. read更新缓存(旧值)

6.2 延迟双删策略

为了避免这种并发执行情况,保证数据最终一致性,可以在update操作中增加一个延迟删除操作。通过延迟删除,可以使删除缓存操作位于并发执行的最后,保证缓存在数据库更新后被删除,下次读取时就可以获取最新数据:

public void update() {
	// 删除缓存
	// 更新数据库
	// 删除缓存
	// 延迟删除:如DelayQueue或MQ
}

6.3 串行化策略

上述延迟双删策略保证了数据的最终一致性,但是并发read到延迟删除这一时间段的数据仍可能是不一致的。

如果需要保证数据的强一致性,就需要将update和read操作串行化了。

例如可以使用BlockingQueue,在update和read时将实际逻辑添加到队列中,然后按顺序进行执行。

其实,如果对数据一致性要求十分严格的话,可以直接不适用缓存,直接从数据库读取,那么就不会有一致性问题了。

7 如何解决缓存雪崩、缓存穿透和缓存击穿?

缓存雪崩、缓存穿透和缓存击穿都是指,在高峰期系统处理大量请求时,由于缓存中获取不到数据,从而直接请求数据库,造成数据库崩溃的现象。

7.1 缓存雪崩

缓存雪崩是指在高峰期,由于缓存服务器宕机或者大量key同时过期,大量业务请求数据库,造成数据库崩溃。

为了避免缓存服务器宕机造成的缓存雪崩,需要开启Redis的主从复制、哨兵模式和集群功能,保证系统的高可用。为了宕机后的数据恢复,还需要开启持久化功能。

在业务中,应该设置不同的过期时间,避免大量key过期后被同时删除。

如果缓存服务器已经宕机,则需要开启本地缓存,通过限流或服务降级,避免数据库崩溃。后续通过Redis重启从磁盘恢复数据,在恢复原先的架构。

7.2 缓存穿透

缓存穿透是指大量请求的数据在缓存和数据库中都不存在,每次请求都会越过缓存请求数据库,但是数据库中也不存在数据,因此不会更新缓存。

这种情况往往是恶意的,我们可以通过缓存一个空值状态到数据库,下次请求时,直接响应该空值:

public Result find() {
	// 查询缓存
	// 缓存如果存在,直接返回
	// 缓存不存在,查询数据库
	// 数据库如果存在,直接返回
	// 数据库不存在,更新对应空值状态缓存并返回
}

由于不存在的数据是无穷多个的,我们没办法缓存每个空值状态。可以采取一种反向思维,使用布隆过滤器缓存已存在的数据,请求查询时,先判断布隆过滤器中是否存在:

  • 存在:从对应缓存或数据库中获取数据。
  • 不存在:直接返回空值状态

新增数据时:

public void add(Data data) {
	// 更新数据库
	// 添加到布隆过滤器
}

查询数据时:

public Result find() {
	// 查询布隆过滤器
	// 布隆过滤器不存在,直接返回空值状态
	// 布隆过滤器存在,查询缓存
	// 缓存如果存在,直接返回
	// 缓存不存在,查询数据库,更新缓存并返回
}

7.3 缓存击穿

缓存击穿是指某个热点key过期后,大量请求集中式并发访问,此时这些请求都会直接请求数据库,容器造成数据库崩溃。

对于这种热点数据,可以将其设置成永不过期。或者使用定时任务,在过期前适当延长过期时间或更新缓存。

8 Redis为什么那么快?

Redis是个单线程程序,它的所有数据都存储在内存中,使用NIO多路复用与客户端进行交互,保证客户端的高并发请求;使用单线程处理指令,避免了线程的上下文切换。

当客户端发送请求时,请求指令会被缓存到指令队列中,Redis工作线程会轮询指令队列进行执行,将指令结果缓存到响应队列,Redis工作线程轮询会响应队列通过对应套接字响应给指定客户端。

这里的指令队列和响应队列,都是NIO的操作系统内核缓存,依赖底层操作系统。

9 Redis分布式锁

Redis分布式锁是日常项目中常用的工具。

所谓锁,就是一种全局唯一的状态标识,并且对其操作必须是原子性的。

在单机场景下,我们可以使用同一个Java对象保证它是全局唯一的,由于Java的synchronized等操作保证了加锁/解锁操作是原子性的,所以它可以作为锁。

在分布式场景下,由于Redis是单线程操作的,所有指令都会按顺序执行,天然保证了加锁/解锁的原子性。同时Redis的key是唯一的(就算是集群中也是唯一的),这就保证了使用Redis作为分布式锁的可能性。

我们可以通过set指令(原子操作)设置分布式锁:

# 1、获取分布式锁
set disKey disValue ex 5 nx
# 2、如果响应成功,说明加锁成功,进行业务操作
# 3、业务操作完成,解锁
del disKey

需要注意分布式锁的过期时间,如果分布式锁过期了,但是业务还没有完成,那么该分布式锁就失效了。

此时可以考虑使用Redisson,它会使用看门狗机制,定时检查客户端是否仍持有锁,并且适当延长过期时间。

posted @ 2023-02-01 17:15  Xianuii  阅读(179)  评论(0编辑  收藏  举报