分布式项目-数据库与缓存一致性问题

    由于在自身学习的过程中,会涉及到数据库与缓存修改的问题,查询了很多资料,写在这里的目的也是做一个总结,完善自身.会根据下面几个方面进行,因为在分布式项目中,做不到CAP理论, 具体的一致性问题依然是需要根据项目的需求来决定使用哪一种方案,所以下面的几种情况只是做一个参考使用,至于 CAP 理论, 可以参考大神文章:  http://www.ruanyifeng.com/blog/2018/07/cap.html 这个不是本章需要阐述的重点.


1.没有缓存的情况下 数据库主从出现不一致问题

发生不一致的场景:

    用户1.进行写 sale= 100 的操作,到主数据库;

    在binlog还没进行同步的情况下, 用户2来进行读取操作, 这时候读取的值就是 sale = 50

    用户1的值 binlog到从库成功

这时候就会出现读取的数据是旧值的情况,

这种不一致的解决方法:

方案一: 忽略

        如果业务可以接受短暂的不同步问题,其实不解决是最好的解决方案,任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。不要把系统架构搞得太复杂.

方案二:强制读主

        使用一个高可用的主库提供读写服务,读和写都落在主库上,采用缓存来提升系统的读性能

方案三:选择性读主库

1. 使用redis 来做一层拦截  key 可以使用 库名:表名:id名  来进行存储, 设置一个过期时间, 为主从同步的过期时间

2.用户1在写操作的时候, 在redis进行一层存储, 新数据落入主库

3.用户2在 查询的时候 先进行 redis查询, 如果 存在表示 这段时间刚发生过写操作,从库还未进行同步完成, 这时候直接进行主库的数据查询操作,如果没有这个key 表示同步完成, 直接可以查询 从库

总结:

1.如果可以业务可以接受短暂不同步,可不修改

2.强制读主,高可用主库,用缓存提高读性能

3.在cache里记录哪些记录发生过写请求,来路由读主还是读从

    该思想源于: http://zhuanlan.51cto.com/art/201807/578180.htm


2.有缓存情况下,数据库与缓存不一致问题

2.1先讨论,当有数据需要变动的时候, 是 set缓存还是 delete(淘汰)缓存 ?

问:KV缓存都缓存了一些什么数据?

答:

        基本类型的数据,例如:int

        序列化后的对象,例如:User实体,本质是binary

        文本数据,例如:json或者html

问:淘汰缓存中的这些数据,修改缓存中的这些数据,有什么差别?

答:

        淘汰某个key,操作简单,直接将key置为无效,但下一次该key的访问会cache miss

        修改某个key的内容,逻辑相对复杂,但下一次该key的访问仍会cache hit

问:缓存中的value数据一般是怎么修改的?

答:

        基本类型的数据,直接set修改后的值即可

        序列化后的对象:一般需要先get数据,反序列化成对象,修改其中的成员,再序列化为binary,再set数据

        json或者html数据:一般也需要先get文本,parse成doom树对象,修改相关元素,序列化为文本,再set数据

结论:对于对象类型,或者文本类型,修改缓存value的成本较高,一般选择直接淘汰缓存。

问:对于基本类型的数据,究竟应该修改缓存,还是淘汰缓存?

答:视情况而定

案例1:

假设,缓存里存了某一个用户uid=123的余额是money=100元,业务场景是,购买了一个商品pid=456。

分析:如果修改缓存,可能需要:

去db查询pid的价格是50元

去db查询活动的折扣是8折(商品实际价格是40元)

去db查询用户的优惠券是10元(用户实际要支付30元)

从cache查询get用户的余额是100元

计算出剩余余额是100 - 30 = 70

到cache设置set用户的余额是70

为了避免一次cache miss,需要额外增加若干次db与cache的交互,得不偿失。

结论:此时,应该淘汰缓存,而不是修改缓存。

案例2:

假设,缓存里存了某一个用户uid=123的余额是money=100元,业务场景是,需要扣减30元。

分析:如果修改缓存,需要:

从cache查询get用户的余额是100元

计算出剩余余额是100 - 30 = 70

到cache设置set用户的余额是70

为了避免一次cache miss,需要额外增加若干次cache的交互,以及业务的计算,得不偿失。

结论:此时,应该淘汰缓存,而不是修改缓存。

案例3:

假设,缓存里存了某一个用户uid=123的余额是money=100元,业务场景是,余额要变为70元。

分析:如果修改缓存,需要:

到cache设置set用户的余额是70

修改缓存成本很低。

结论:此时,可以选择修改缓存。当然,如果选择淘汰缓存,只会额外增加一次cache miss,成本也不高。

总结:

允许cache miss的KV缓存写场景:

大部分情况,修改value成本会高于“增加一次cache miss”,因此应该淘汰(delete)缓存

2.2 数据有变动, 是先删除缓存再更新db, 还是先更新db再删除缓存?

读操作,如果没有缓存,流程是怎么样的?

答:1.先get缓存,如果缓存中没有,2.从数据库获取数据,读从库,读写分离3.把数据set到缓存,未来能够读取缓存

写操作,流程是怎么样的?

答:写操作,既要操作数据库中的数据,又要操作缓存里的数据。

这里,有两个方案:

先操作数据库,再操作缓存;

先操作缓存,再操作数据库;

并且,希望保证两个操作的原子性,要么同时成功,要么同时失败。

这演变为一个分布式事务的问题,保证原子性十分困难,很有可能出现一半成功,一半失败,接下来看下,当原子性被破坏的时候,分别会发生什么。

一、先操作数据库,再操作缓存

如果这两步操作,原子性被破坏, 更新db成功,而第二步失败, 会导致,数据库里是新数据,而缓存里是旧数据,业务无法接受。如果第一步就更新失败,直接就报错,也不会存在问题,

二、先操作缓存,再操作数据库

如果这两步操作,原子性被破坏:

这里分为两种情况: set缓存 / delete缓存

如果是set 缓存: 这时候缓存中的数据是最新的,可是更新db失败,会导致db的数据不是最新的,数据不一致,业务无法接受.并且,一般来说,数据最终的一致性是以db为标准,写缓存成功,并不算真正的成功.

如果是使用delete 缓存: 第一步成功,第二步更新db失败,会导致缓存中没有数据,数据库是之前的数据,缓存中没有数据,数据没有不一致,对业务没有影响,只是下一次读取的时候,会多一次 cache miss.

最终,先操作缓存,还是先操作数据库?

答:

(1) 读请求,先读缓存,如果没有,读数据库,再set回缓存

(2) 写请求

       1. 先缓存,再数据库

       2. 缓存,使用delete,而不是set

数据库与缓存不一致的解决方案,可以参考下面的文章<缓存与数据库不一致的解决方案> :

https://blog.csdn.net/u012129558/article/details/52278091

你可能感兴趣的:(分布式项目-数据库与缓存一致性问题)