From 7d80794455eda54a2a82a8a134b6f9f2318cfa04 Mon Sep 17 00:00:00 2001 From: yanglbme Date: Sun, 18 Nov 2018 23:03:15 +0800 Subject: [PATCH] docs(redis): add redis-consistence.md --- README.md | 6 +- docs/high-concurrency/redis-cas.md | 7 ++ docs/high-concurrency/redis-consistence.md | 78 ++++++++++++++++++ .../redis-production-environment.md | 7 ++ img/redis-junior-inconsistent.png | Bin 0 -> 10977 bytes 5 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 docs/high-concurrency/redis-cas.md create mode 100644 docs/high-concurrency/redis-consistence.md create mode 100644 docs/high-concurrency/redis-production-environment.md create mode 100644 img/redis-junior-inconsistent.png diff --git a/README.md b/README.md index ef9b81c..7b52615 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ - [Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?](/docs/high-concurrency/redis-persistence.md) - [Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点?](/docs/high-concurrency/redis-cluster.md) - [了解什么是 Redis 的雪崩和穿透?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md) -- 如何保证缓存与数据库的双写一致性? -- Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗? -- 生产环境中的 Redis 是怎么部署的? +- [如何保证缓存与数据库的双写一致性?](/docs/high-concurrency/redis-consistence.md) +- [Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?](/docs/high-concurrency/redis-cas.md) +- [生产环境中的 Redis 是怎么部署的?](/docs/high-concurrency/redis-production-environment.md) ### 分库分表 - 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的? diff --git a/docs/high-concurrency/redis-cas.md b/docs/high-concurrency/redis-cas.md new file mode 100644 index 0000000..4a4cae1 --- /dev/null +++ b/docs/high-concurrency/redis-cas.md @@ -0,0 +1,7 @@ +## 面试题 + + +## 面试官心理分析 + + +## 面试题剖析 diff --git a/docs/high-concurrency/redis-consistence.md b/docs/high-concurrency/redis-consistence.md new file mode 100644 index 0000000..e850a02 --- /dev/null +++ b/docs/high-concurrency/redis-consistence.md @@ -0,0 +1,78 @@ +## 面试题 +如何保证缓存与数据库的双写一致性? + +## 面试官心理分析 +你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? + +## 面试题剖析 +一般来说,就是如果你的系统**不是严格要求**“缓存+数据库”必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,即:**读请求和写请求串行化**,串到一个**内存队列**里去,这样就可以保证一定不会出现不一致的情况。 + +串行化之后,就会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 + +### Cache Aside Pattern +最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。 +- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 +- 更新的时候,**先删除缓存,然后更新数据库**。 + +**为什么是删除缓存,而不是更新缓存?** + +原因很简单,很多时候,复杂点的缓存的场景,缓存不单单是数据库中直接取出来的值。 + +比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。 + +另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于**比较复杂的缓存数据计算的场景**,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,**这个缓存到底会不会被频繁访问到?** + +举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次,100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。**用到缓存才去算缓存。** + +其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。 + +### 最初级的缓存不一致问题及解决方案 +问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 + +![redis-junior-inconsistent](/img/redis-junior-inconsistent.png) + +解决思路:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。 + +### 比较复杂的数据不一致问题分析 +数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,**查到了修改前的旧数据**,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了... + +**为什么上亿流量高并发场景下,缓存会出现这个问题?** + +只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就**可能会出现上述的数据库+缓存不一致的情况**。 + +**解决方案如下:** + +更新数据的时候,根据**数据的唯一标识**,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。 + +一个队列对应一个工作线程,每个工作线程**串行**拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 + +这里有一个**优化点**,一个队列中,其实**多个更新缓存请求串在一起是没意义的**,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 + +待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。 + +如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值 + +高并发的场景下,该解决方案要注意的问题: +- 读请求长时阻塞 + +由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回 + +该解决方案,最大的风险点在于说,**可能数据更新很频繁**,导致队列中积压了大量更新操作在里面,然后**读请求会发生大量的超时**,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的。 + +另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要**部署多个服务**,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据,这个时候就导致**读请求的长时阻塞**。 + +一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。 + +- 读请求并发量过高 + +这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值 + +但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。 + +- 多服务实例部署的请求路由 + +可能这个服务部署了多个实例,那么必须**保证**说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 nginx 服务器**路由到相同的服务实例上**。 + +- 热点商品的路由问题,导致请求的倾斜 + +万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。 \ No newline at end of file diff --git a/docs/high-concurrency/redis-production-environment.md b/docs/high-concurrency/redis-production-environment.md new file mode 100644 index 0000000..4a4cae1 --- /dev/null +++ b/docs/high-concurrency/redis-production-environment.md @@ -0,0 +1,7 @@ +## 面试题 + + +## 面试官心理分析 + + +## 面试题剖析 diff --git a/img/redis-junior-inconsistent.png b/img/redis-junior-inconsistent.png new file mode 100644 index 0000000000000000000000000000000000000000..d8df1cd23b8ff6f8e88a807ee63ad59467027a03 GIT binary patch literal 10977 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 X$}2$C1;KNwC=^CjONFR>>*0R`{U9w| literal 0 HcmV?d00001