Redis总结

879 阅读29分钟

1. 什么是Redis?

Redis (Remote Dictionary Server) 是一个使用 C 语言编写的,开源的高性能非关系型(NoSQL)的键值对数据库。

与传统数据库不同的是Redis的数据是存在内存中的,所以读写速度非常快,因此Redis被广泛应用于缓存方向,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value 数据库。

2. Redis的数据结构是什么样的?

Redis 可以存储键和不同类型数据结构值之间的映射关系。键的类型只能是字符串,而值除了支持最基础的五种数据类型外,还支持一些高级数据类型。

(1)String

String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

  • 缓存功能:String字符串是最常用的数据类型,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

  • 计数器:使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。

  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率。

(2)Hash

这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

(3)List

List 是有序列表

  • 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

  • 文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

(4)Set

Set 是无序集合,会自动去重。 可以基于 Set 实现交集、并集、差集的操作。可以把两个人的好友列表整一个交集,查询共同好友等等。

(5)Sorted Set

Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。

有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

  • 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。比如微博热搜榜,就是有个后面的热度值,前面就是名称

  • 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

(6)Bitmap

位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter)。

(7)HyperLogLog

供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV。

(8)Geospatial

可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。可以用Redis来实现附近的人或者计算最优地图路径。

3. 缓存雪崩、击穿、穿透

(1)缓存雪崩

同一时间大量缓存失效,此时大量请求直接来到数据库,造成短期内系统的cpu和内存压力激增,严重时甚至造成宕机。

解决方案

  • 在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值,这样可以保证数据不会在同一时间大面积失效
  • 集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题
  • 设置热点数据永远不过期,有更新操作就更新缓存
  • 为了避免缓存雪崩打崩数据库,可以预先设置本地缓存(ehcache) + 限流(hystrix),保证服务能正常工作的

(2)缓存穿透

查询一个数据库中不存在的数据(如ID为负数),此时缓存一定不存在,所以就会绕过Redis请求数据库,如果有恶意查询,请求量大也会给数据库带来压力。

解决方案

  • 在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等
  • 对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过这个方法可能导致缓存中存储大量无用数据。
  • 使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。

布隆过滤器(BloomFilter)

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。 image.png

(3)缓存击穿

某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。

解决方案

  • 可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
  • 使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
  • 针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效

4. Redis是如何进行持久化的?

Redis的数据全部存储在内存中,如果突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是Redis的持久化机制,它会将内存中的数据库状态保存到磁盘中。Redis主要有两种持久化机制:RDB和AOF。

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。 tips:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

RDB与AOF的对比

(1)RDB
  • 原理 fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
  • 优点 RDB会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据。

RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

  • 缺点 RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性会更好。

同时RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,对客户端的使用影响很大。

(2)AOF
  • 优点 AOF是一秒一次去通过一个后台的线程fsync操作,最多丢这一秒的数据。

AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复。

  • 缺点 一样的数据,AOF文件比RDB还要大。

AOF开启后,Redis支持写的QPS会比RDB支持写的要低,需要每秒都要去异步刷新一次日志(fsync),当然即使这样性能还是很高。

如何抉择?

单独用RDB会丢失很多数据,单独用AOF,数据恢复没RDB来的快,最好两者配合使用,第一时间用RDB恢复,然后AOF做数据补全,冷备热备一起上,才是互联网时代一个高健壮性系统的王道。

另外在Redis重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

5. Redis集群

(1)主从复制

指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是单向的,只能由主节点到从节点。Redis 主从复制支持主从同步和从从同步两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

同步过程

启动一台slave 的时候,他会发送一个psync同步命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新的数据通过AOF发送给slave。

主要作用
  • 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)。
  • 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  • 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。
优点
  • 读写分离
  • Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
  • Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
  • Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据
缺点
  • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

(2)哨兵模式

当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,可以使用哨兵模式,利用哨兵工具来实现自动化的系统监控和故障恢复功能。 image.png 上图 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;

  • 数据节点: 主节点和从节点都是数据节点; 在复制的基础上,哨兵实现了自动化的故障恢复功能,下方是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。

  • 自动故障转移(Automatic failover): 当主节点不能正常工作时,哨兵会开始自动开始故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

  • 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。

  • 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。

哨兵工作方式
  • 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。

  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)。

  • 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态。

  • 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)。

  • 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。

  • 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

  • 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

(3)Redis-Cluster集群

redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现redis的分布式存储,也就是说每台redis节点上存储不同的内容。

Redis-Cluster采用无中心结构,它的特点如下:

  • 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。

  • 节点的fail是通过集群中超过半数的节点检测失效时才生效。

  • 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

  • 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出。

  • 高可用: 集群支持主从复制和主节点的自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。

工作方式

在Redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

为了保证高可用,Redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。

集群对比
  • 哨兵模式着眼于高可用,在Master宕机时会自动将Slave提升为Master,继续提供服务
  • Redis-Cluster集群着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。

5. Redis过期键的删除策略

Redis的过期策略,主要有定期删除和惰性删除两种。

  • 定期删除:默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
  • 惰性删除:查询的时候判断当前的Key有没有过期,过期就删除,且不会返回,没过期就返回。

Redis中的内存淘汰机制

  • noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)

  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。

  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。

  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。

  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。

  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

Redis的LRU算法实现

LRU.png

6. Redis数据结构底层-SDS

Redis是C语言开发的,C语言自己就有字符类型,但是Redis却没直接采用C语言的字符串类型,而是自己构建了动态字符串(SDS)的抽象类型。

比如set test testValue命令,其实在Redis里创建了两个SDS,一个是名为test的Key SDS,另一个是名为testValue的Value SDS,就算是字符类型的List,也是由很多的SDS构成的Key和Value罢了。

SDS在Redis中除了用作字符串,还用作缓冲区(buffer)。

SDS结构

struct sdshdr{
     int len;
     int free;
     char buf[];
   }

sds.png

SDS与C字符串的区别

  • 计数方式不同 C语言对字符串长度的统计,就完全来自遍历,从头遍历到末尾,直到发现空字符就停止,以此统计出字符串的长度,这样获取长度的时间复杂度来说是0(n),Redis自己本身就保存了长度的信息,所以我们获取长度的时间复杂度为O(1)。
  • 杜绝缓冲区溢出 进行字符串拼接时,C是不记录字符串长度的,一旦我们调用了拼接的函数,如果没有提前计算好内存,是会产生缓存区溢出的。而Redis结构存储了当前长度,还有free未使用的长度,在做拼接操作前,会去判断是否可以放得下,如果长度够就直接执行,如果不够,就进行扩容。
  • 减少修改字符串时带来的内存重分配次数 空间预分配:当我们对SDS进行扩展操作的时候,Redis会为SDS分配好内存,并且根据特定的公式,分配多余的free空间,还有多余的1byte空间(这1byte也是为了存空字符),这样就可以避免我们连续执行字符串添加所带来的内存分配消耗。

惰性空间释放:当我们执行完一个字符串缩减的操作,redis并不会马上收回我们的空间,因为可以预防你继续添加的操作,这样可以减少分配空间带来的消耗,但是当你再次操作还是没用到多余空间的时候,Redis还是会收回对应的空间,防止内存的浪费。

  • 二进制安全 C语言是判断空字符去判断一个字符的长度的,也就是’\0‘,但是有很多数据结构经常会穿插空字符在中间,比如图片,音频,视频,压缩文件的二进制数据。而Redis保存了字符串的长度,不需要判断空字符,直接判断长度即可,所以redis也经常被我们拿来保存各种二进制数据。

7. Redis的Keys命令

使用keys指令可以扫出指定模式的key列表。不过Redis的单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用 SMEMBERS 命令可以返回集合键当前包含的所有元素。对于SCAN这类增量式迭代命令来说, 在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证。

8. Redis实现延时队列

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

9. Redis高级用法

(1)pub/sub

功能是订阅发布功能,可以用作简单的消息队列。可以实现 1:N 的消息队列,不过在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如RocketMQ等。

异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。另外,不使用sleep的话,list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

(2)Pipeline

可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。

(3)Lua

Redis支持提交Lua脚本来执行一系列的功能,比如实现分布式锁。

(4)事务

Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

10. Redis与Memcache的区别

Memcache的特点:

  • MC 处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,性能非常优秀;
  • MC 功能简单,使用内存存储数据;
  • MC 的内存结构以及钙化问题我就不细说了,大家可以查看官网了解下;
  • MC 对缓存的数据可以设置失效期,过期后的数据会被清除;
  • 失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;
  • 当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。

Memcache的缺点:

这些限制在现在的互联网场景下很致命,成为大家选择Redis的重要原因

  • key 不能超过 250 个字节;
  • value 不能超过 1M 字节;
  • key 的最大失效时间是 30 天;
  • 只支持 K-V 结构,不提供持久化和主从同步功能。

Memcache与Redis的区别:

  • 与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  • Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
  • 相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  • Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

11. Redis在项目全周期中如何高效使用?

  • 事前:Redis高可用,主从+哨兵,Redis-cluster,避免全盘崩溃
  • 事中:本地ehcache缓存+Hystrix限流+降级,避免Mysql服务挂掉
  • 事后:Redis持久化RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据

12. 缓存有哪些类型?

  • 本地缓存 本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
  • 分布式缓存 分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
  • 多级缓存 实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。这也是最常用的缓存方案,单考单一的缓存方案往往难以撑住很多高并发的场景。

13. 如何保证缓存一致性?

(1)先删缓存,再更新数据库

先删除缓存,数据库还没有更新成功,此时如果读取缓存,缓存不存在,去数据库中读取到的是旧值,缓存不一致发生。

解决方案

延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。 缓存一致性1.png 流程如下:

  • 线程1删除缓存,然后去更新数据库
  • 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
  • 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
  • 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值

(2)先更新数据库,再删除缓存

更新数据库成功,如果删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致。

解决方案

image.png

  • 先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。不过引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦。就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的。 image.png
  • 可以借助监听binlog的消息队列来做删除缓存的操作。这样做的好处是,不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。当然,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。

(3)设置缓存过期时间

每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。

如果对于一致性要求不是很高的情况,可以采用这种方案。这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。

为什么是删除,而不是更新缓存?

以先更新数据库,再删除缓存来举例。如果是更新的话,那就是先更新数据库,再更新缓存。举个例子:如果数据库1小时内更新了1000次,那么缓存也要更新1000次,但是这个缓存可能在1小时内只被读取了1次,那么这1000次的更新有必要吗?反过来,如果是删除的话,就算数据库更新了1000次,那么也只是做了1次缓存删除,只有当缓存真正被读取的时候才去数据库加载。

14. Redis分布式锁

锁是一种用来解决多个执行线程访问共享资源错误或数据不一致问题的工具。而随着互联网世界的发展,单体应用已经越来越无法满足复杂互联网的高并发需求,转而慢慢朝着分布式方向发展。所以同样,我们需要引入分布式锁来解决分布式应用之间访问共享资源的并发问题。

使用场景

  • 避免不同节点重复相同的工作:比如用户执行了某个操作有可能不同节点会发送多封邮件;
  • 避免破坏数据的正确性:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现;