目录

  • 高实时性服务
  • 缓存+数据库双写不一致
  • 最初级【删除缓存失败】
  • 复杂【数据库更新未完成】
  • 起因
  • 解决:串行化
  • 串行化方案注意问题
  • 读请求长时阻塞
  • 读请求并发量过高
  • 多服务实例部署的请求路由
  • 热点路由问题


高实时性服务

  1. 实时性比较高的数据缓存 – 库存服务
  2. 库存会更新,每次修改都要去更新缓存数据;库存数据在缓存中一旦过期,或者是被清理掉了,前端的nginx服务就会发送请求给库存服务,去数据库中获取相应的数据。
  3. 更新数据库完成,直接更新redis缓存

上面流程就会涉及到一个问题,数据库与缓存双写,数据不一致的问题

缓存+数据库双写不一致

最初级【删除缓存失败】

  1. 问题先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
  2. 解决思路先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

复杂【数据库更新未完成】

起因

mysql双写一致性解决方案 数据库双写不一致_缓存

1. 库存变更,先删除缓存,再去更新数据库,此时还没修改成功

2. 一个读请求过来,去读缓存,发现缓存空了,去查询数据库,查到了还未更新成功的旧数据,放到了缓存中

3. 然后之前未更新的数据更新成功,造成数据库和缓存中的数据不一样了。

为什么上亿流量高并发场景下,缓存会出现这个问题?

  1. 只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题
  2. 如果并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种不一致的场景
  3. 但如果每天的是上亿的流量每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况
  4. 高并发了以后,问题是很多的

解决:串行化

数据库与缓存更新与读取操作进行异步串行化。

mysql双写一致性解决方案 数据库双写不一致_mysql双写一致性解决方案_02

1. 更新数据的时候,根据数据的唯一标识(hash或其他),将写操作路由之后,发送到一个jvm内部队列

2. 读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送到同一个jvm内部的队列

3. 一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行

4. 这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新,此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可

  1. 等队列对应的工作线程完成了上一个写操作(数据库更新库存)之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
  2. 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值

串行化方案注意问题

高并发的场景下,该解决方案要注意的问题

读请求长时阻塞

由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回

  1. 该解决方案,最大的风险点在于说,数据如果更新频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量读请求直接走数据库

实际中务必通过模拟真实的各种情形测试,看看更新数据的频繁具体程度

  1. 另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作
  2. 如果一个内存队列里挤压100个商品的库存修改操作,每个库存更新操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据

这个时候就导致读请求的长时阻塞

  1. 一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的
  2. 如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少

其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的

  1. 针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了
  2. 比如实际中,每秒500的写操作并发,分成5份,目标实现每200ms完成 100个写操作
  3. 单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成
  4. 那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了
  5. 如果写QPS扩大到10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,共200个队列;
  6. 大部分的情况下,大量的读请求过来,都是直接走缓存取到数据的
  7. 少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面
  8. 等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据

读请求并发量过高

  1. 突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值
  2. 但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大
  3. 1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作
  4. 如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存
  5. 一般来说,1:1,1:2,1:3,500个写请求对应每秒钟按1:2会有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回
  6. 在同一时间最多hang住的可能也就是单机200个读请求,同时hang住,单机hang200个读请求,还是ok的
  7. 如果1:20算,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万,1万个读请求全部hang在库存服务上,就死定了

多服务实例部署的请求路由

mysql双写一致性解决方案 数据库双写不一致_缓存_03

当服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上

热点路由问题

热点商品请求的路由问题,导致请求的倾斜

  1. 如果某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大
  2. 就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大
  3. 但是的确可能某些机器的负载会高一些