如何保持mysql和redis中数据的一致性?

https://zhihu.com/question/319817091/answer/2428216275

问题

本人刚简单地学习了一下redis,了解了它的出现背景和基本用法,对于不轻易改变的数据,首次可以将其从mysql中取出存到redis中,以后只要判断redis有没有这个数据,有的话直接拿来用就行了。那么,如果在redis获取这个数据以后,我到mysql中更新了数据,那么redis中的数据不就和mysql不一致了吗?怎么让redis中的数据和mysql保持实时一致呢?

回答1

基础

什么是Redis

首先大体理解:Redis是一个基于内存,通过键值对(key-value)的形式来存储数据的NoSQL数据库。和其相相对应的是MySQL,一种SQL类型的数据库。

它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些[数据类型]都支持push/popadd/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中(所以也就避免了对硬盘的频繁操作)。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

为什么要使用Redis

前面提到了对于Redis来说一是支持的类型比较多,二是由于将信息存储在内存上,所以速度上会比SQL类型的数据库快上很多。

实际上,对于Redis来说读取速度能够达到110000次/s,写入的速度是81000次/s,能够满足我们更高频率的请求与承担更高的并发,在一些大型网站,也都会使用到redis进行数据的缓存处理,无论是采用到原生的数据类型(使用到redis自带的一些方法),或是封装成自己需要的缓存结构(进行二次的封装,这里可以参见我的博客Redis数据封装)ps:后续会进行修正与完善,在面对当下的高并发请求,都能够起到比较好的效果。

下面从两个具体的方面进行讲解:

缓存提高访问速度

一般我们对于存储在数据库中的数据的请求,都是前端页面发送请求信息,后端接收请求,并对数据进行封装和处理,最后再调用SQL语句,执行sql语句取到具体的信息。但是其实对于MySQL来说数据都是存储在硬盘上,进行数据的读取势必会消耗一定时间,但是计算机的运行速度比我们想象的要快很多,很多时候数据都是在极短的时间里面就能够查询出来,并返回给前端页面。

但是我们需要思考的是对于用户的每一次请求都需要从数据库中进行读取,当请求过于庞大时候,就会导致数据的读取和页面的相应不能够成正比,最后的结果就是,在一些特殊的情况下,根本就不能够在短时间内获取到信息。

对于Redis而言,主要就是读取速度快,而且数据类型丰富,所以就可以使用Redis来做缓存,主要过程见下图:这个时候,在下一次缓存还没过期时间内再此进行同样的请求就可以减少检索的时间,提高访问的速度。

缓存支持高并发

缓存不仅能够提高访问的速度还有一点就是能够帮助我们减轻高并发情况下的数据库压力:

一般用在什么地方(不考虑五种数据结构)

  1. 用作对MySQL数据库中数据的缓存,在请求到达时候,先判断缓存中是否有对应的数据,没有数据时候再请求到数据库中,然后将数据同步到Redis中,供下一次的请求到达时候,能够在数据不更改时候直接请求到Redis缓存,来减轻数据库的压力,例如对于某明星的具体个人信息,可以将其存取在缓存中,对于此类数据不需要每次都去请求数据库,可以将对数据库的访问留给跟紧急的数据。
  2. 在秒杀场景下,通过进行设置,将商品的信息预先加载到缓存中,通过异步加载的方式,先将数据库中商品的剩余数目加载出来,展示到页面上,同时也能够防止超卖的情况出现。
  3. 可以进行对整个页面的预先加载和缓存到Redis中,在用户进行访问时候,先将页面整体信息加载出来,然后加载数据。

数据类型与各自使用场景

数据类型

你只要说你看过Redis,或者是用过Redis,面试官一般都是问这个问题来检测你是只会数据的Set和 Get,还是对Redis的数据都有所了解与学习,所以在这个问题尽管看起来很简单,将有几种数据结构回答出来,然后各自的使用场景举例出来,但是要回答具体且对于有些特殊数据类型的底层实现进行了解和阐述还是有一定难度的,所以对于这个问题要重视起来。

首先是五种的数据类型: 包括StringListSetZsetHash。下面是具体情况的具体操作。

数据类型底层的数据类型可以存储的值可以进行的操作使用在哪些场景下面
StringSDS(后续讲解)字符串,整数,浮点数对字符串或者其中的一部分进行系类的操作处理使用最为频繁,一般就是用作简单的键值对的缓存处理作为key值1. 可以用于保存单个字符串或是JSON字符串类型。2. 因为string类型是二进制安全的所以可以把一个图片文件的内容 作为字符串来存储。3. 计算器进行使用时候,对微博数,粉丝数目进行增和减。4. increment 本身就具有原子操作的特性。
List压缩列表列表1. 从两端压入或者弹出元素。2. 对单个或者多个元素进行修建,只保留一个返回内的元素存储一些列表类型的数据结构。1. 消息队列 可以使用reids的lpush 与 brpop 来实现阻塞队列,保证负载均衡与高可用。2. 文章列表,每个用户都有数据自己的文章列表,需要分页展示文章列表,使用list 不但有序 还可以支持按照索引范围获取元素。最新消息排行等。
Setinsert无序不重复集合添加、获取、移除单个元素检查一个元素是否存在于集合中计算交集、并集、差集从集合里面随机获取元素1, 对于两个集合之间的数据进行交集,并集差集运算等。 2. 方便实现对于共同好友的采集,共同关注,二度好友等,对于以上说的集合操作,还可以使用不同的命令将结果返回给客户端或则是存放在一个新的集合里面。 3. 利用唯一性, 统计访问网站的所有独立IP信息。
Hash压缩列表键值对集合添加、获取、移除单个键值对获取所有键值对检查某个键是否存在1. 用于存储一个对象2. 存储用户用户的具体信息,年龄,生日等数据
Zset跳表(后续讲解)有序不重复集合添加、获取、删除元素根据分值范围或者成员来获取元素计算一个键的排名1.排行榜,比如在进行文章发布时候,可以将其发布时间作为score进行存储。这样时候获取时候就是自动按照时间排序好。2 可以在进行班级成绩排序时候,不需要再进行排序函数的排序,而是将学号作为value 将成绩作为score,这样就是自然的为我们排好序。3 进行权重的权衡 例如对于 微博热搜榜等

所以综上所述,在回答此问题时候,先说明具体的五种数据类型,也可以说出比较有特点的String和Zset的底层数据结构,然后表明具体都有什么特点最后再说明具体的使用的位置

String:

最简单的键值对的存储的键值,底层采用到sds实现,提高使用的效率,主要用在一些单点的数据上。

List:

有序的列表,对于文章的展示,或者是说国家的展示和省份的存储,利用其有序的特点。

Hash:

主要是用在对象的存储上,存储商品的信息,个人的具体住址,电话等。

Set:

无序的集合,主要使用在一些交集的查找,有哪些的共同好友,或者说是并集的使用,查询共同的关注,共同的兴趣爱好等。

Zset:

有序的集合,底层采用跳表的实现方式,拥有和set功能的功能的同时还能够及时的进行排序处理,或者说是在使用的时候,就可以利用其有序的特点进行省略排序的步骤

事务

对于事务来说,是数据库都不能够绕开的点,但是不同于MySQL中事务特点,对于Redis中的事务确实截然不同的。

Redis中的事务是什么样子

对于Redis中的事务来说:所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

与其他事务一样也是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

如何实现

在Redis中通过几个特殊的命令来实现事务,分别是MULTIEXECWATCHDISCARD,在事务执行的过程中,能够执行多条指令,但是一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。所以总结来说就是对于在Redis中的事务是顺序执行下去的一个队列命令。

实现的过程

首先来具体了解一下每条语句所对应的含义:

MULTI: 表示事务的开始,在redis中执行这条语句以后,表示事务的开启,这个时候,所输入的命令并不会立马执行下去,相反,在未出现EXEC特殊字符时候,所有命令的执行都会进入一个队列中。

EXEC:表示对进入到队列的语句进行一个执行操作,执行的是先进先出的原则。

WATCH: 表示监听,可以监听一个或多个健,是一个乐观锁,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令

DISCARD: 表示清空事务队列,前面我们提到了事务在未被执行的过程中,都会进入到一个队列中,此条操作就会情况事务队列,并放弃执行事务。

所以执行的三个阶段:

  1. 事务开始 MULTI
  2. 命令入队
  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队,如果收到以上的请求,就会跳出继续进入队列,进行执行相关联的语句。

遇到问题时候

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行

ACID

原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency) 事务前后数据的完整性必须保持一致。

隔离性(Isolation) 多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

具有的特性

尽管Redis的事务也叫做事务,但是Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有持久性。

Redis持久化的两种方法

对于这个问题在面试的情况也是切身经历过,对于这个问题考察还是蛮有深度的,因为不仅仅是死死记住就行,需要自己切实理解其中的缘由与原理,因为这个题目衍生出来的问题比较多,所以只有自己全都是理解,才能够做到在面试时候不会被卡住:这部分想要具体详细了解可以参看《Redis设计与实现》。

什么是

Redis是一个支持持久化的内存数据库,通过持久化可以把在内存中的数据同步到硬盘上,来保证数据的持久化,当Redis在重启时候,可以通过加载硬盘文件重新加载数据到内存中,达到数据恢复的目的:

RDB

是Redis默认的持久化方法,按照一定的时间周期策略把位于内存中的数据保存为RDB文件(是一个二进制的文件)有两个命令可以进行RDB文件的生成:sava 和BGsave在执行save命令时候会阻塞Redis服务器进程,在执行过程中,Redis服务器不能够处理任何其他的请求。但是BGsave不同的是,在执行时候会派生出一个子进程由子进程完成RDB文件的创建,服务器父进程继续完成其他的响应。 bgrewriteaof
执行时候状态:

在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。

其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。最后,BGREWRITEAOF和BGSAVE两个命令不能同时执行:

❑如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。

❑如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。因为BGREWRITEAOF和BGSAVE两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑——并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,就很容易导致问题的出现。

优缺点

优点

1、只有一个文件 dump.rdb,方便持久化。 2、容灾性好,一个文件可以保存到安全的磁盘。 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能 4.相对于数据集大时,比 AOF 的启动效率更高。

缺点

1.是RDB是生产快照文件的 五分钟会生成一个快照文件,但是若是在这五分钟内出现了故障就会导致 数据的丢失会丢失五分钟之内的数据, 对于 AOF来说 最多 丢失一秒的数据

  1. 在生成数据快照的时候 如果文件很大 ,客户端可能会暂停执行几毫秒 此时若是正好在做秒杀活动的时候 fork一个子进程去生成一个大的快照 就会出现问题。

AOF

AOF持久化是通过保存redis服务器所指向的写命令来记录数据库状态。通过Write函数追加到文件的最后,当redis在重启时候会通过重新执行文件中保存的写命令来再内存中重建整个数据库内容。当两个同时开启时候,优先选择AOF进行数据库的恢复。对于 AOF来说是追加的方式,所以没有磁盘的寻址开销 会更快,像 mysql中 binlog。

优缺点

优点

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。 2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。 3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点

一样的数据 时候 AOF 文件比 RDB 还要大,在AOF开启以后 Reids 支持写入的 APS会比 RDB支持下的要低 异步刷新一次日志 fsync 。

注意: AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

如何选择

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

缓存异常情况见解

缓存穿透

定义:量请求的key根本就不存在于缓存中,此时就会去请求数据库,没有经过Redis的缓存。若是这样的请求过于庞大,就会导致数据库的请求过于频繁,若是一个黑客想要对数据库进行攻击,就可以制造大量不存在的key去访问数据库,造成数据库访问压力过大,最后导致宕机。

解决办法:因为请求的是数据库不存在的信息,所以避免的就是对于哪些请求原本就不存在在数据库里面的信息的请求过滤掉。

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

缓存雪崩

定义:是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方法:可以看到不同于穿透来说,缓存原来是存在的,后来失效了,所以就会导致请求都打在了数据库,所以需要做的就是,尽量避免在短时间里面所有的key都失效。

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。

缓存击穿

定义:是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

缓存预热

定义:系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

内存问题

Redis 的内存淘汰策略有哪些

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis主要消耗的是什么资源

因为Redis那么快的一个主要的原因就是基于内存,所以消耗的也主要是内存资源

在内存用完之后会发生什么

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis为什么那么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

多路 I/O 复用模型

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

重要的数据结构

布隆过滤器

前面提到可以使用到布隆过滤器来解决缓存穿透的问题

什么是布隆过滤器

布隆过滤器我们可以把他看做由二进制向量(或者说是数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时所使用一些基础的数据结构而言例如 SETLISTMAP,它更加快捷,占用的空间更小,但是效率也更高。也有缺点就是其返回的结果是概率性的,不能够完完全全保证结果的准备性,理论上来说,加入更多的元素其误差性也就越大。而且,加入到布隆过滤器中的数据很难够删除。 在布隆过滤器中的每一个元素都只占用1bit,或者说,每一个元素在其中的存在都是0: 不存在,1: 存在。

元素加入

当一个元素想要加入布隆过滤器时候,需要进行以下的操作:

  • 使用 布隆过滤器中的哈希函数对元素值进行计算,得到不同的哈希值(有几个哈希函数参与计算就会得到几个哈希值)
  • 根据得到的哈希值,在相对应的地址上填上对应的1。

元素判断

想要查看一个元素是否存在于布隆过滤器中:

  • 对此值进行相同的哈希计算,得到几个不同的哈希值。
  • 将得到的哈希值在布隆过滤器中相对应位置进行查找,若是所查找到的所有值都为1 说明该元素在布隆过滤器中是存在的,若是有一个值不为1,说明该元素不存在于布隆过滤器。 综上所述: 当输入一个值时候,布隆过滤器说其在其中可能会出现误判,有时候会有几率性存在,但是布隆过滤器说其不在其中

使用场景:

1: 判断给定的数据是否存在:可以进行大数据的判断,判断某一数值是否存在于一个很大的数字集合中(亿为单位),防止redis的缓存穿透(判断请求的数据是否有效,能够避免访问绕过缓存直接请求数据库)垃圾邮件的过滤,黑名单的功能等等。 2: 去重: 比如爬给定网址时候对已经爬取过的URL进行去重。

布隆来解决Redis

知道了布隆过滤器的原理和工作特写以后,可以把所有可能会请求到的key 放到布隆过滤器中,不存在就会直接返回错误信息给客户端,若是存在信息才会继续下面的流程。

String的底层实现SDS

是一个SDS 其里面有三个变量 一个

free为0 表示SDS没有分配任何未使用空间 len 表示 存储字符串的长度 buf 表示存储的char 类型的数组 以空字符结尾 好处 :

  1. 获取字符串的长度时候 复查度为1 因为维持了一个长度的变量。
  2. 杜绝了缓存区的溢出 当sds的api操作对其进行修改的时候 其会先检查此时的空间满不满足修改所需要的空间大小,若是 不满足 会自动扩展空间到修改值的大小。
  3. 避免了内存的重分配 因为对于 c 来说 当增加字符串的时候 要先进行内存的重新分配来扩展底层的数组的空间 不然会出现 缓冲区的溢出。在截取的时候 还是要重新分配内存空间的大小释放不再使用的那部分空间 不然会出现内存的泄露。对于 sds 来说会有内存空间的预分配 会为其分配额外的空间的大小
  4. 惰性空间释放 当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
  5. 相比与基础的数据来说 也可以使用一部分 stdio.h 中的库函数

Zset的底层实现跳表

首先来看一张跳表的底层实现图:

对于上图中最左边叫做zskiplist包含以下的属性

header: 指向跳跃表的表头节点。

tail: 指向跳跃表的表尾节点。

level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。

length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

对于右边的叫做zskiplistNode包含下面的属性 level(层): 节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。

后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。

分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。

注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层

为什么不用红黑树而是用跳表(面试真题)

  1. 我们都知道 对于红黑树来说 查找的时候 在找到了指定范围的小值以后 还要进行中序遍历的方式顺序继续寻找其他不超过大致的节点 但是对于 调表来说 找到了小值以后 对第一层链表进行若干步的遍历即可实现。
  2. 插入和删除来说 对于 树结构的删除 与 加入就会很有可能导致树的结构的变化 逻辑复杂 但是对于 调表来说 删除 与插入 就是 修改相邻的节点就好。 算法实现上来说 跳表就相对来说简单了很多 红黑树就会复杂很多

主从一致性

如何保持主从一致性

对于主从复制而言就是说 由于 对于 一台的主服务器既要满足于写入的操作 也要完成 读的操作 压力会比较大, 这个时候就需要从服务器连接上主服务器进行复制操作 ,当启动一个sync 的命令 给master 当这个从服务器第一次连接到主服务器时候 会触发一个全局的复制操作 ,master就会启动一个线程,生成 RDB快照 还会把所有的写请求 都缓存进入到内存中 RDB文件生成以后 master会发送给从服务器 slave 接收到rdb以后 是 先写进本地的磁盘,然后再度加载到内存里面。 以上是使用到 sync时候的情况 但是会有很多的弊端 出现 若是 在命令传播的阶段主从服务器因为网络的问题出现了中断 此时有需要重新开始复制的操作 ,就算之前有点已经复制过来了但是还是要再次进行全部复制 这一点上可以进行优化处理,并且 对于 sync来说 是一个很消耗资源的操作

  1. 生 rdb文件时候 会消耗主服务器大量的cpu 内存以及磁盘的io资源
  2. 主服务器将这些rdb文件发送给从服务器的时候 会消耗主从服务器的网络资源 并且1对于 请求的响应 会有较大的影响。
  3. 接收到 rdb文件以后 在载入的期间时候 从服务器会因为阻塞而无法处理命令的请求。 这个时候出现了 psync 命令 有完整重同步 与 部分重同步 : 完成的和之前的sync一样 在使用 bgsave 时候 产生 rdb文件 并发送 并且将位于 主缓存总的写命令发送给从服务器来进行同步 部分同步时候: 当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。 以上我们就可以在使用时候 直接使用到 psync 来解决在出现中断时候的问题

### reids 如何保证redis的缓存与数据库的一致性

在我们使用到缓存时候 就可能会涉及到 缓存与 数据库双存储双写的情况,要是出现双写的情况 就可能会出现数据库的一致性问题 这个时候 该怎么解决一致性问题。 最好也是不要使用这个方案: 读请求写请求串行化 串行化到一个内存队列里面去。 使用到串行化可以保证一定不会出现不一致的情况,但是却是得系统的吞吐量大大降低,用比正常情况下的机器去支持一个请求过程。 最经典的缓存+数据库读写的模式, 就是 :读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

为什么提到了先删除缓存 而不是更新缓存呢?

就是说 对于一个缓存来说 并不是单单从数据库中直接取出来的 怎么理解呢 就是说有些的缓存已经存在了 然后我们要对其进行一个更改 或者是两个表的共同数据合并的计算才会得到这样的一个缓存的值,但是对于我们来说 更新缓存的代价是很高昂的 。我们每次对于 数据库的更新都要去更新缓存的话 有时候 对于 这个缓存来说 其实访问到的次数并没有那么多 (缓存的作用是1在读取的时候 能够 直接读取到的) 但是对于某些数据 我么你只是对其进行一个修改 读取时候 很少 、这个时候 我们若只是删除缓存,在一段时间里面 存储不过就会从新计算一次 得到一个新的值,用到的时候才去计算 不用频繁更新 不用做复杂的运算 减少开销

Key相关问题

过期淘汰策略

对于已经过期的键来说 reids提供了三种的过期删除策略:

  • 定时删除 在设置键的过期时间的同时,创建一个定时器,让定时器在键值过期时间来临时,立即执行对键的删除操作。
  • 惰性删除: 放任过期的键值不管,只要在从键值空间进行访问的时候,会监察键值是否过期,若是过期进行删除,没有过期 不会管。
  • 定期删除,我们会隔一段时间对数据库进行一个监察,删除里面过期的键值,至于要删除多少的键值,以及监察多少的数据时候 都是算法来决定的。 下面来讲解好处与不好的地方:

定时:

对内存友好 在键过期以后就会删除 之前占用的内存就会得以释放 但是对CPU来说没那么友好 在过期的键值比较多时候,需要消耗大量的cpu来处理删除这些值。

惰性:

对cpu友好 不会特地消耗cpu在特定的时间里面进行删除 只会在使用的时候进行过期的检查然后删除 但是对于内存没那么友好是因为 有时候 过期的键值 没能够够删除 在很长的一段时间里面 都会占用内存的空间。

定期

前面的两则都有各自的缺点与优点 但是对于定期来说 是一种折中的处理 ❑定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 ❑除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。 但是不确定的难点在于 执行的时常与评率 删除评率太频繁 或者时常太长 就会退化成为定时删除 消耗cpu 执行太少 执行态度 退化为 惰性 浪费内存。

redis热key问题?如何发现以及如何解决?

首先是如何发现这些热点的key

  1. 凭借业务的经验,例如商品在做秒杀的时候 那个商品的key就被看做是热点key 2.在客户端进行收集 可以在操作redis之前加入一行的代码进行数据的统计。统计出哪些的key值访问量较大。
  2. 对于有的jreids集群架构来说会有 proxy 在client 与 redis之间 ,可以在1proxy层进行信息的采集 判断哪些的key值是热点值
  3. 使用redis自带的命令 1、 monitor 命令 而言 可以事实抓取到 redis服务器端收到的命令 这个时候 可以写代码统计 哪些的key值是热点数据, 也可以使用 分析工具进行分析 如 redis-faina 但是 这个方法在高并发的条件下,有内存报增的隐患,也会降低 redis的性能。

该如何进行解决: 一种是 利用二级的缓存来实现 利用一个 ehcache 或者一个hashmap 在发现了热点key以后 将其加载到 系统的jvm中 对于 这样的热点key值的请求 会直接从 jvm中进行取 而不会 走到 redis层。 2 备份热点key值 对于前面来说 我们已经可以获取到了哪些的key值是热点key值,下面要做的就是 将这些的热点key值在多个redis上都保存一份,此时有热点key值进行请求的时候u,在有备份的redis上取值,进行返回 不会导致每次的请求都落在同一台redis上,导致请求不过来的情况发生

什么是一致性Hash算法

就是我们在做数据库分库,分表,或者是分布式缓存时候,都会遇到一个问题就是说: 如何将数据均匀分散在各个节点中,并且尽量的在加减节点时候 使得当前我们的数据受的影响最小?

取模运算

对于取模运算来说也算是我们能够想到的最简单的方法,对于HashMap中值的随机存放也是利用到这种思想(但是实现的方法不同)。

对于取模来说可以将传入的 Key 按照 index=hash(key)%N 这样来计算出需要存放的节点。其中 hash 函数是一个将字符串转换为正整数的哈希映射方法,N 就是节点的数量。

这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性都较差。

比如增加或删除了一个节点时,所有的 Key 都需要重新计算,显然这样成本较高,为此需要一个算法满足分布均匀同时也要有良好的容错性和拓展性。

一致性Hash算法

简而言之就是将所有的Hash值构成一个环,其值的范围在0~2^32-1 :如下图所示,在后续中,所有进行操作过后的值都会分散在这个环上

例如现在我们有A,B,C 三个应用节点(利用IP,或者是hostname)这样的唯一性字段作为Key。

之后我们对数据进行散列,散列在这个环上,为D1,D2,D3。

将数据散列到环上之后,会进行顺时针的查找,顺时针查找出第一个遇到的节点,然后将其映射到遇到的第一个节点上,此时就会出现,D1映射到A节点上,D2映射到B节点上……

容错性

这个时候若是说对于节点B出现了故障,这个时候对于 D2数据与D3数据不会出现问题,也就是说并不会收到任何的影响。但是对于D1来说会映射到B节点上。

扩展性

前面是在一个节点出现宕机的情况,可以发现受影响的也只有D2,其他的没有收到影响,同样,在新添加一个节点的时候:会发现也只有D3收到影响,但是其余部分没有收到影响

虚拟节点

虽然上面的情况,对于该算法都能够完美应对,但是还有一种情况就是节点数量比较少时候,这样很多的数据都会集中在A节点,对于B节点来说数据就会比较少。如下图所示:

这个时候为了能够解决这个问题一致哈希算法引入了虚拟节点。将每一个节点都进行多次 hash,生成多个节点放置在环上称为虚拟节点:在计算时候,在IP后加上特定的编号,来对其进散列,如下图所示,生成虚拟的节点,进行数据的映射

分布式缓存和本地缓存有什么区别

分布式

对于分布式缓存来说 常用的有 redis 与 memchched

  1. 对于 分布式缓存来说 用于在集群下面多个节点共同使用同一份缓存的情况,从而减少数据库的查询。
  2. 分布式缓存可能会受到网络 IO流的影响,因此吞吐率与缓存的数据的大小有很大的关系。
  3. 对于分布式缓存来说 网络 IO流消耗的时间对于 分布式缓存是一个不小的消耗

本地缓存

1.相比于分布式缓存来说 本地的缓存就会高效很多,因为网络 IO流的影响 一次分布式缓存存取数据所消耗的时间可能对于 本地缓存来说 能够存取几千上万次。

  1. 对于本地缓存来说 在集群的条件下可能会存在数据的不一致问题,所以在使用时候需要考虑到缓存的时效性,以及缓存的数据对数据不一致的敏感程度 可以在使用的过程中使用本地缓存来做以及缓存,减少分布式缓存的访问量(网络IO带宽消耗及网络时间消耗) 使用分布式缓存做二级的缓存,减少在集群的环境下访问数据库的次数。

reids 与 memcached有什么区别

  1. 对于 m来说,所有的数据都存储在内存汇总,断电以后会挂掉,数据不能超过内存的大小,局限性比较大,但是 reids有持久化的功能 可以将部分数据存储在硬盘上,保证了数据的安全性。
  2. 对于m来说 所有的值都是字符串类型,支持的数据也比较简单和少,但是redis支持的时刻类型比较多。
  3. reids的速度要比m快很多。
  4. 对于 redis来说 单个value的最大限制是1GB,但是对于m来说只能保存1MB的数据, 因此可以实现的功能也会比较多,简单的消息队列。
  5. Redis是单线程的,采用多路io复用的方式。m是多线程采用的是非阻塞io。

哨兵模式

参考:掘金

主从复制可能会出现的问题

  • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。
  • 原生复制的弊端在早期的版本中也会比较突出,比如:redis复制中断后,从节点会发起psync。此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。

解决方案(哨兵模式)

首先了解什么是哨兵模式:

如上图所示,主要功能有主节点存活检测、主从运行情况检测、自动故障转移、主从切换。Redis Sentinel最小配置是一主一从。主要有以下功能:

  • 监控:不断检查主服务器和从服务器是否正常运行。
  • 通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他应用程序发出通知。
  • 自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样人工干预就可以免了。
  • 配置提供者:在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。

工作原理

每个 Sentinel 节点都需要 定期执行 以下任务:

  1. 每个 Sentinel 以 每秒钟 一次的频率,向它所知的 主服务器从服务器 以及其他 Sentinel 实例 发送一个 PING 命令(如下图)。

  1. 如果一个 实例instance)距离 最后一次 有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为 主观下线(如下图)。

  1. 如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 主服务器 的确进入了 主观下线 状态(如下图)。

  1. 如果一个 主服务器 被标记为 主观下线,并且有 足够数量 的 Sentinel(至少要达到 配置文件 指定的数量)在指定的 时间范围 内同意这一判断,那么这个 主服务器 被标记为 客观下线(如下图)。

  1. 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率,向它已知的所有 主服务器 和 从服务器 发送 INFO 命令。当一个 主服务器 被 Sentinel 标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从 10 秒一次改为 每秒一次(如下图)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zNt8HIq-1595943711439)(upload\image-20200728204325355.png)]

  1. Sentinel和其他Sentinel协商 **主节点** 的状态,如果 **主节点** 处于SDOWN` 状态,则投票自动选出新的 主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制(如下图)。

  1. 当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器 的 客观下线状态 就会被移除。当 主服务器 重新向 Sentinel 的 PING 命令返回 有效回复 时,主服务器 的 主观下线状态 就会被移除(如下图)。

Redis 事物

Redis 事物介绍:

  1. redis事务可以一次执行多个命令,本质是一组命令的集合。

2.一个事务中的所有命令都会序列化,按顺序串行化的执行而不会被其他命令插入

作用:一个队列中,一次性、顺序性、排他性的执行一系列命令

multi指令的使用

1. 下面指令演示了一个完整的事物过程,所有指令在exec前不执行,而是缓存在服务器的一个事物队列中

2. 服务器一旦收到exec指令才开始执行事物队列,执行完毕后一次性返回所有结果

3. 因为redis是单线程的,所以不必担心自己在执行队列是被打断,可以保证这样的“原子性”

注:redis事物在遇到指令失败后,后面的指令会继续执行

# Multi 命令用于标记一个事务块的开始事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性( atomic )地执行
> multi(开始一个redis事物)
incr books
incr books
> exec (执行事物)
> discard (丢弃事物)

[root@redis ~]# redis-cli
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set test 123
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379> get test
"123"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set test 456
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get test
"123"
127.0.0.1:6379>

注:mysql的rollback与redis的discard的区别

1. mysql回滚为sql全部成功才执行,一条sql失败则全部失败,执行rollback后所有语句造成的影响消失

2. redis的discard只是结束本次事务,正确命令造成的影响仍然还在.

1)redis如果在一个事务中的命令出现错误,那么所有的命令都不会执行;      2)redis如果在一个事务中出现运行错误,那么正确的命令会被执行

watch 指令作用

实质:WATCH 只会在数据被其他客户端抢先修改了的情况下通知执行命令的这个客户端(通过 WatchError 异常)但不会阻止其他客户端对数据的修改

  1. watch其实就是redis提供的一种乐观锁,可以解决并发修改问题
    watch会在事物开始前盯住一个或多个关键变量,当服务器收到exec指令要顺序执行缓存中的事物队列时,redis会检查关键变量自watch后是否被修改
    3. WATCH 只会在数据被其他客户端抢先修改了的情况下通知执行命令的这个客户端(通过 WatchError 异常)但不会阻止其他客户端对数据的修改

watch+multi实现乐观锁

[

setnx指令(redis分布式锁)

1、分布式锁

SETNX命令(SET if Not eXists) 语法: SETNX key value 功能: 当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。  1. 分布式锁本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试 2. 占坑一般使用 setnx(set if not exists)指令,只允许一个客户端占坑 3. 先来先占,用完了在调用del指令释放坑

> setnx lock:codehole true
.... do something critical ....
> del lock:codehole

4. 但是这样有一个问题,如果逻辑执行到中间出现异常,可能导致del指令没有被调用,这样就会陷入死锁,锁永远无法释放

5. 为了解决死锁问题,我们拿到锁时可以加上一个expire过期时间,这样即使出现异常,当到达过期时间也会自动释放锁
> setnx lock:codehole true
> expire lock:codehole 5
.... do something critical ....
> del lock:codehole

6. 这样又有一个问题,setnx和expire是两条指令而不是原子指令,如果两条指令之间进程挂掉依然会出现死锁

7. 为了治理上面乱象,在redis 2.8中加入了set指令的扩展参数,使setnx和expire指令可以一起执行
> set lock:codehole true ex 5 nx
''' do something '''
> del lock:codehole

redis解决超卖问题

1、使用reids的 watch + multi 指令实现

1)原理

  1. 当用户购买时,通过 WATCH 监听用户库存,如果库存在watch监听后发生改变,就会捕获异常而放弃对库存减一操作
    . 如果库存没有监听到变化并且数量大于1,则库存数量减一,并执行任务

2)弊端

  1. Redis 在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行
    . 保证商品的库存量正确是一件很重要的事情,但是单纯的使用 WATCH 这样的机制对服务器压力过大

2、使用reids的 watch + multi + setnx指令实现

1)为什么要自己构建锁

  1. 虽然有类似的 SETNX 命令可以实现 Redis 中的锁的功能,但他锁提供的机制并不完整
    且setnx也不具备分布式锁的一些高级特性,还是得通过我们手动构建

2)创建一个redis锁

  1. 在 Redis 中,可以通过使用 SETNX 命令来构建锁:rs.setnx(lock_name, uuid值)
    锁要做的事情就是将一个随机生成的 128 位 UUID 设置位键的值,防止该锁被其他进程获取

3)释放锁

  1. 锁的删除操作很简单,只需要将对应锁的 key 值获取到的 uuid 结果进行判断验证
    合条件(判断uuid值)通过 delete 在 redis 中删除即可,rs.delete(lockname)

3. 此外当其他用户持有同名锁时,由于 uuid 的不同,经过验证后不会错误释放掉别人的锁

4)解决锁无法释放问题

1. 在之前的锁中,还出现这样的问题,比如某个进程持有锁之后突然程序崩溃,那么会导致锁无法释放

2. 而其他进程无法持有锁继续工作,为了解决这样的问题,可以在获取锁的时候加上锁的超时功能

优化锁无法释放的问题,为锁添加过期时间

def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):
    '''
    rs: 连接对象
    lock_name: 锁标识
    acquire_time: 过期超时时间
    locked_time: 锁的有效时间
    return -> False 获锁失败 or True 获锁成功
    '''
    identifier = str(uuid.uuid4())
    end = time.time() + expire_time
    while time.time() < end:
        # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
        if rs.setnx(lock_name, identifier): # 尝试取得锁
            # print('锁已设置: %s' % identifier)
            rs.expire(lock_name, locked_time)
            return identifier
        time.sleep(.001)
    return False

其他

Redis常见性能问题和解决方案?

  1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
  2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
  3. 尽量避免在压力较大的主库上增加从库
  4. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
  5. 一个字符串类型的值能存储最大容量是多少? 512M

10亿个key 找出 10w个开头

答:keys 指令扫描 对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题? 这个时候要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用Redis做过异步队列吗,是如何实现的

使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

Redis集群的最大节点数是多少?

16384个

什么是Redis哈希槽

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

如何保证缓存与数据库双写时的数据一致性?

只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况

串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。

场景描述解决方案
先写缓存,再写数据库,缓存写成功,数据库写失败缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
需要缓存异步刷新指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

https://docs.qq.com/doc/DS3B2eWN1YWRKbFZU​docs.qq.com/doc/DS3B2eWN1YWRKbFZU

回答2

这实际上是个“如果要做的足够精致是非常难的“问题。缓存失效被称为计算机科学里最难的两个问题之一(另外一个是起名字)。

先对本题一致性做个说明。这里的不一致是指:假如一个数据访问者同时读取Redis和DB,他能在一段时间里发现二者不一样。

不错,如果一份数据放在DB,然后copy到Redis,然后改DB,那么Redis是不会自己魔幻般同步变更的。必须有某种机制告诉Redis该变了。这些机制包括(但不仅仅限于):

1. Redis里的数据不立刻更新,等redis里数据自然过期。然后去DB里取,顺带重新set redis。这种用法被称作“Cache Aside”。好处是代码比较简单,坏处是会有一段时间DB和Redis里的数据不一致。这个不一致的时间取决于redis里数据设定的有效期,比如10min。但如果Redis里数据没设置有效期,这招就不灵了。

2. 更新DB时总是不直接触碰DB,而是通过代码。而代码做的显式更新DB,然后马上del掉redis里的数据。在下次取数据时,模式就恢复到了上一条说的方式。这也算是一种Cache Aside的变体。这要做的好处是,数据的一致性会比较好,一般正常情况下,数据不一致的时间会在1s以下,对于绝大部分的场景是足够了。但是有极少几率,由于更新时序,下Redis数据会和DB不一致(这个有文章解释,这里不展开)。

Cache Aside,就是“Cache”在DB访问的主流程上帮个忙

1和2的做法常规上被称为“Cache“。而且因为1有更新不及时的问题,2有极端情况下数据会不一致的问题,所以常规Cache代码会把1+2组合起来,要求Redis里的数据必须有过期时间,并且不能太长,这样即便是不一致也能混过去。同时如果是主动对数据进行更新,Cache的数据更新也会比较及时。

并且2并不一定总是行得通。比如OLTP的服务在前面是Cache+DB的模式,而数据是由后台管理系统来更新的,总是不会触碰OLTP服务,更不会动Cache。这时将Redis看作是存储也算是一种方案。就是:

3. Redis里的数据总是不过期,但是有个背景更新任务(“定时执行的代码” 或者 “被队列驱动的代码)读取db,把最新的数据塞给Redis。这种做法将Redis看作是“存储”。访问者不知道背后的实际数据源,只知道Redis是唯一可以取的数据的地方。当实际数据源更新时,背景更新任务来将数据更新到Redis。这时还是会存在Redis和实际数据源不一致的问题。如果是定时任务,最长的不一致时长就是更新任务的执行间隔;如果是用类似于队列的方式来更新,那么不一致时间取决于队列产生和消费的延迟。常用的队列(或等价物)有Redis(怎么还是Redis),Kafka,AMQ,RMQ,binglog,log文件,阿里的canal等。

Cache当作“存储”来用,访问者只看得到Cache

这种做法还有一种变体Write Through,写入时直接写DB,DB把数据更新Cache,而读取时读Cache。

Write Through + Cache当存储

以上方式无论如何都会有一段时间Redis和DB会不一致。实践上,这个不一致时间短则几十ms,长可以到几十分钟。这种程度的一致性对于很多业务场景都已经足够了。很多时候,用户无法区分自己读取的是Redis还是DB,只能读取到其中的一个。这时数据看起来直觉上是没问题的就可以接受了。只要不出现,用户先看见了数据是A,然后看到数据是B,之后一刷新,又看到A的尴尬场景就行了。(这也可以部份解释为啥用经常使用共享式的Cache而不是本地Cache方案)。

但对于有些业务,比如协作文档编辑,电商秒杀的扣库存,银行转账等,以上的做法就不够用了。解决办法也有两大类。第一种是不要用Redis,只用DB。或者更直接点说是“只要一个单点的数据源”。这样肯定就没有一致性问题,代价就是CAP中因为CP被满足,因此A被牺牲掉。这就是为啥银行一系统升级就要停服务的原因。

当然实际上也有CAP兼顾,但是C要的强一点,A就得弱一点,但不至于完全牺牲掉的做法。这里不展开。

另外一种保证一致性的做法就是用某种分布式协议一致性来做,大致可以归结到

  1. SAGA或者TCC - 这两种需要业务代码的大量配合。通过业务代码来补偿一致性。
  2. 2PC, 3PC - 现实当中有XA协议。比如Ehcache是支持XA协议的。但是性能表现不佳,运维也麻烦,我比较少见到实际这么干的。
  3. 基于Paxos或者Raft的分布式锁,然后对Redis和DB进行双写,但是除非客户端和服务器么次都去访问分布式锁,也会有一点点不一致的问题。这实际上相当于将多个地方的一致性控制交给了分布式锁的集中维护。

这些做法实施复杂度和运维复杂度太高,以至于对于像Redis + DB这种场景基本上没人这么干。本质上大家用Redis一般也就是想做个Cache而已。这些方案通常被用到比如多数据中心数据一致性维护的系统中。

综上,除了单点DB存储之外的方案,其一致性面临的窘境是

  • 要么,接受“最终一致”,但到底多久之后一致,不一致时表现怎么样,有很多种做法。分布式一致性有各种各样的模型,比如线性一致性、顺序一致性等。他们都是在“不一致”和“强一致”之间提供某种折衷。这些折衷大量应用于我们常见的诸多业务之中、如社交、IM、电商不触及钱的地方等
  • 要么,要求必须强一致。那么在分布式条件下就要牺牲A。比如访问一个Cache,Cache知道自己的数据不是最新的,就要和DB去Sync,Sync的过程中DB的数据还不能改。期间访问者要不收到一个错误“数据不同步,不能访问”,要不就卡在那里等着同步完成。个人以为,这还不如干脆就不要Cache,在维护强一致的同时,用其他方式来优化访问性能。

最最后提醒下,本文有很多不严谨的地方,包括对Cache的形式总结其实只有典型的几种,实际可能的要多得多;再比如对一致性的介绍也非常粗浅,原因是为了让初学者有一点点概念,能看得进去(就这样,已经很长了,评论区里也有人表示接受不了)。

对于分布式和其一致性的完整知识的学习需要耗费大量的精力,Good Luck & Best Wishes。

回答3

如何保证缓存和数据库一致性,这其实是一个老生常谈的话题了。

但很少人能真正把这个问题讲明白,例如:

  • 到底是更新缓存还是删缓存?
  • 到底选择先更新数据库,再删除缓存,还是先删除缓存,再更新数据库?
  • 为什么要引入消息队列保证一致性?
  • 延迟双删到底什么?会有什么问题?到底要不要用?
  • ...

下面我们就来把这些问题「彻底」讲清楚。

内容稍微有点长,但干货很多,希望你可以耐心读完。

引入缓存提高性能

我们从最简单的场景开始讲起。

如果你的业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可,这时你的架构模型是这样的:

但随着业务量的增长,你的项目请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。

这个阶段通常的做法是,引入「缓存」来提高读性能,架构模型就变成了这样:

当下优秀的缓存中间件,当属 Redis 莫属,它不仅性能非常高,还提供了很多友好的数据类型,可以很好地满足我们的业务需求。

但引入缓存之后,你就会面临一个问题:之前数据只存在数据库中,现在要放到缓存中读取,具体要怎么存呢?

最简单直接的方案是「全量数据刷到缓存中」:

  • 数据库的数据,全量刷入缓存(不设置失效时间)
  • 写请求只更新数据库,不更新缓存
  • 启动一个定时任务,定时把数据库的数据,更新到缓存中

这个方案的优点是,所有读请求都可以直接「命中」缓存,不需要再查数据库,性能非常高。

但缺点也很明显,有 2 个问题:

  1. 缓存利用率低:不经常访问的数据,还一直留在缓存中
  2. 数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率)

所以,这种方案一般更适合业务「体量小」,且对数据一致性要求不高的业务场景。

那如果我们的业务体量很大,怎么解决这 2 个问题呢?

缓存利用率和一致性问题

先来看第一个问题,如何提高缓存利用率?

想要缓存利用率「最大化」,我们很容易想到的方案是,缓存中只保留最近访问的「热数据」。但具体要怎么做呢?

我们可以这样优化:

  • 写请求依旧只写数据库
  • 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
  • 同时,写入缓存中的数据,都设置失效时间

这样一来,缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。

再来看数据一致性问题。

要想保证缓存和数据库「实时」一致,那就不能再用定时任务刷新缓存了。

所以,当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。

但数据库和缓存都更新,又存在先后问题,那对应的方案就有 2 个:

  1. 先更新缓存,后更新数据库
  2. 先更新数据库,后更新缓存

哪个方案更好呢?

先不考虑并发问题,正常情况下,无论谁先谁后,都可以让两者保持一致,但现在我们需要重点考虑「异常」情况。

因为操作分为两步,那么就很有可能存在「第一步成功、第二步失败」的情况发生。

这 2 种方案我们一个个来分析。

1) 先更新缓存,后更新数据库

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。

虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。

这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。

2) 先更新数据库,后更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。

之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。

这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。

可见,无论谁先谁后,但凡后者发生异常,就会对业务造成影响。那怎么解决这个问题呢?

别急,后面我会详细给出对应的解决方案。

我们继续分析,除了操作失败问题,还有什么场景会影响数据一致性?

这里我们还需要重点关注:并发问题

并发引发的一致性问题

假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?

有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:

  1. 线程 A 更新数据库(X = 1)
  2. 线程 B 更新数据库(X = 2)
  3. 线程 B 更新缓存(X = 2)
  4. 线程 A 更新缓存(X = 1)

最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。

也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。

同样地,采用「先更新缓存,再更新数据库」的方案,也会有类似问题,这里不再详述。

那怎么解决这个问题呢?这里通常的解决方案是,加「分布式锁」。

两个线程要修改「同一条」数据,每个线程在改之前,先去申请分布式锁,拿到锁的线程才允许更新数据库和缓存,拿不到锁的线程,返回失败,等待下次重试。

这么做的目的,就是为了只允许一个线程去操作数据和缓存,避免并发问题。

除此之外,我们从「缓存利用率」的角度来评估这个方案,也是不太推荐的。

这是因为每次数据发生变更,都「无脑」更新缓存,但是缓存中的数据不一定会被「马上读取」,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。

而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列「计算」得出一个值,才把这个值才写到缓存中。

由此可见,这种「更新数据库 + 更新缓存」的方案,不仅缓存利用率不高,还会造成机器性能的浪费。

所以此时我们需要考虑另外一种方案:删除缓存

删除缓存可以保证一致性吗?

删除缓存对应的方案也有 2 种:

  1. 先删除缓存,后更新数据库
  2. 先更新数据库,后删除缓存

同样地,先来看「第二步」操作失败的情况。

先删除缓存,后更新数据库,第二步操作失败,数据库没有更新成功,那下次读缓存发现不存在,则从数据库中读取,并重建缓存,此时数据库和缓存依旧保持一致。

但如果是先更新数据库,后删除缓存,第二步操作失败,数据库是最新值,缓存中是旧值,发生不一致。所以,这个方案依旧存在问题。

总之,和前面提到的问题类似,第二步失败依旧有不一致的风险。

好,我们再来看「并发」问题,这个问题是我们需要关注的「重点」。

1) 先删除缓存,后更新数据库

如果有 2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

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

依旧是 2 个线程并发「读写」数据:

  1. 缓存中 X 不存在(数据库 X = 1)
  2. 线程 A 读取数据库,得到旧值(X = 1)
  3. 线程 B 更新数据库(X = 2)
  4. 线程 B 删除缓存
  5. 线程 A 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。

这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?

其实概率「很低」,这是因为它必须满足 3 个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)

仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。

这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

所以,我们应该采用这种方案,来操作数据库和缓存。

好,解决了并发问题,我们继续来看前面遗留的,第二步执行「失败」导致数据不一致的问题

如何保证两步都执行成功?

前面我们分析到,无论是更新缓存还是删除缓存,只要第二步发生失败,那么就会导致数据库和缓存不一致。

保证第二步成功执行,就是解决问题的关键

想一下,程序在执行过程中发生异常,最简单的解决办法是什么?

答案是:重试

是的,其实这里我们也可以这样做。

无论是先操作缓存,还是先操作数据库,但凡后者执行失败了,我们就可以发起重试,尽可能地去做「补偿」。

那这是不是意味着,只要执行失败,我们「无脑重试」就可以了呢?

答案是否定的。现实情况往往没有想的这么简单,失败后立即重试的问题在于:

  • 立即重试很大概率「还会失败」
  • 「重试次数」设置多少才合理?
  • 重试会一直「占用」这个线程资源,无法服务其它客户端请求

看到了么,虽然我们想通过重试的方式解决问题,但这种「同步」重试的方案依旧不严谨。

那更好的方案应该怎么做?

答案是:异步重试。什么是异步重试?

其实就是把重试请求写到「消息队列」中,然后由专门的消费者来重试,直到成功。

或者更直接的做法,为了避免第二步执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。

到这里你可能会问,写消息队列也有可能会失败啊?而且,引入消息队列,这又增加了更多的维护成本,这样做值得吗?

这个问题很好,但我们思考这样一个问题:如果在执行失败的线程中一直重试,还没等执行成功,此时如果项目「重启」了,那这次重试请求也就「丢失」了,那这条数据就一直不一致了。

所以,这里我们必须把重试消息或第二步操作放到另一个「服务」中,这个服务用「消息队列」最为合适。这是因为消息队列的特性,正好符合我们的需求:

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的需求)

至于写队列失败和消息队列的维护成本问题:

  • 写队列失败:操作缓存和写消息队列,「同时失败」的概率其实是很小的
  • 维护成本:我们项目中一般都会用到消息队列,维护成本并没有新增很多

所以,引入消息队列来解决这个问题,是比较合适的。这时架构模型就变成了这样:

那如果你确实不想在应用中去写消息队列,是否有更简单的方案,同时又可以保证一致性呢?

方案还是有的,这就是近几年比较流行的解决方案:订阅数据库变更日志,再操作缓存

具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。

那什么时候操作缓存呢?这就和数据库的「变更日志」有关了。

拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。

订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:

  • 无需考虑写消息队列失败情况:只要写 MySQL 成功,Binlog 肯定会有
  • 自动投递到下游队列:canal 自动把数据库变更日志「投递」给下游的消息队列

当然,与此同时,我们需要投入精力去维护 canal 的高可用和稳定性。

如果你有留意观察很多数据库的特性,就会发现其实很多数据库都逐渐开始提供「 订阅变更日志」的功能了,相信不远的将来,我们就不用通过中间件来拉取日志,自己写程序就可以订阅变更日志了,这样可以进一步简化流程。

至此,我们可以得出结论,想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做

主从库延迟和延迟双删问题

到这里,还有 2 个问题,是我们没有重点分析过的。

第一个问题,还记得前面讲到的「先删除缓存,再更新数据库」导致不一致的场景么?

这里我再把例子拿过来让你复习一下:

2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

第二个问题:是关于「读写分离 + 主从复制延迟」情况下,缓存和数据库一致性的问题。

如果使用「先更新数据库,再删除缓存」方案,其实也发生不一致:

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。

看到了么?这 2 个问题的核心在于:缓存都被回种了「旧值」

那怎么解决这类问题呢?

最有效的办法就是,把缓存删掉

但是,不能立即删,而是需要「延迟删」,这就是业界给出的方案:缓存延迟双删策略

按照延时双删策略,这 2 个问题的解决方案是这样的:

解决第一个问题:在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。

解决第二个问题:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。

这两个方案的目的,都是为了把缓存清掉,这样一来,下次就可以从数据库读取到最新值,写入缓存。

但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?

  • 问题1:延迟时间要大于「主从复制」的延迟时间
  • 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间

但是,这个时间在分布式和高并发场景下,其实是很难评估的。

很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。

所以你看,采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。

所以实际使用中,我还是建议你采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。

可以做到强一致吗?

看到这里你可能会想,这些方案还是不够完美,我就想让缓存和数据库「强一致」,到底能不能做到呢?

其实很难。

要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。

相反,这时我们换个角度思考一下,我们引入缓存的目的是什么?

没错,性能

一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。

而且,就拿我们前面讲到的方案来说,当操作数据库和缓存完成之前,只要有其它请求可以进来,都有可能查到「中间状态」的数据。

所以如果非要追求强一致,那必须要求所有更新操作完成之前期间,不能有「任何请求」进来。

虽然我们可以通过加「分布锁」的方式来实现,但我们也要付出相应的代价,甚至很可能会超过引入缓存带来的性能提升。

所以,既然决定使用缓存,就必须容忍「一致性」问题,我们只能尽可能地去降低问题出现的概率。

​同时我们也要知道,缓存都是有「失效时间」的,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。

总结

好了,总结一下这篇文章的重点。

1、想要提高应用的性能,可以引入「缓存」来解决

2、引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」

3、更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,解决方案是加「分布锁」,但这种方案存在「缓存资源浪费」和「机器性能浪费」的情况

4、采用「先删除缓存,再更新数据库」方案,在「并发」场景下依旧有不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估

5、采用「先更新数据库,再删除缓存」方案,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据最终一致

6、采用「先更新数据库,再删除缓存」方案,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率

后记

本以为这个老生常谈的话题,写起来很好写,没想到在写的过程中,还是挖到了很多之前没有深度思考过的细节。

在这里我也分享 4 点心得给你:

1、性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案

2、掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题

3、失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案

4、订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致

很多一致性问题,都会采用这些方案来解决,希望我的这些心得对你有所启发。

这篇文章是以「尽可能保证缓存和数据库一致性」这个角度写的,尽可能列出可能遇到的场景、问题、方案优劣。

实际环境为了成本考虑,也可以不用实现得这么复杂,但你需要知道哪块没做到,会有什么问题,并且业务可以容忍这些问题带来的影响,如果不能容忍,文章提供的思路你可以借鉴一下。总之,可以根据场景选择适合自己的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值