缓存与数据库一致性问题

服务端为什么要叫缓存?

如果在不加缓存,则客户端的请求则直接打在了db 数据库上,当有一天服务端接收大量查询,则可能导致数据返回慢或者宕机;

对于频繁读取操作, 缓存在缓存中,以减少对数据库的访问压力,从而提高服务端的访问数;
在这里插入图片描述
一般请求下的请求会如下:

客户端请求到server端,server 则会判断去查缓存是否存在, 如果不存在,则直接查询db,并将db查询结构返回客户端

如果存在, 则直接从缓存中查询, 并将缓存的数据返回客户端;

从代码片段

 public Object getGoodsInfo(String goodsId) {
        //查询缓存是否存在数据
        Object cacheObj = getCache(goodsId);
        if (cacheObj == null) {
            //缓存中不存在数据,则访问数据库
            Object dbObject = selectDbById(goodsId);
            setCache(goodsId, dbObject);
            return selectDbById(goodsId);
        }
        return cacheObj;
    }

但是这样会造成问题:

缓存穿透,缓存击穿,缓存雪崩

缓存穿透

1.当一个不存在的id的查询,则会先如果缓存同时不存在则会直接访问db,但db查询也不存在, 多次这样的查询, 每次都要去数据库在查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库, 这样就缓存穿透了;

a、**接口校验。**在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

b、缓存特定标识。当访问缓存和DB都没有查询到值时,可以将特定标识(如;200, 0 等)写进缓存,但是设置较短的过期时间,该时间需要业务特性来设置。

c、布隆过滤器。 布隆过滤器的原理可以自行百度,我也只知道他是跟多次hash,根据概率来返回结果。

缓存击穿

2.当一个key值,在某一时刻发生高并发量的访问(如key值时间过期,或者突然成为热点数据), 访问缓存不存在,则直接访问db, 造成瞬时数据库请求量大,可能会造成卡顿或者宕机; 造成缓存击穿

a、热数据,设置永远不过期。

b、互斥锁加锁对请求进行同步。锁内逻辑:再次查询缓存,查不到转查数据库并且进行数据缓存,后面的请求就直接缓存,也避免再次查数据库。

缓存雪崩

由于大量缓存失效或者缓存整体不能提供服务,导致大量的请求到达db,会使db负载增加(大量的请求;

a.过期时间,每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效

b.数据预热,对于即将来临的大量请求, 知道那些会成为热key,将数据提前缓存在Redis中,并设置不同的过期时间。

c. 多级缓存, 本地缓存+ redis+ 其他缓存等;

d. 加锁的方式: 互斥锁重建缓存(如redis 的lua 分布式锁等),避免大量请求访问db;

redis lua 分布式锁

伪代码:

 public Object getGoodsInfoLock(String goodsId) {
        //查询缓存是否存在数据
        Object cacheObj = getCache(goodsId);
        if (cacheObj != null) {
            return cacheObj;
        }
        try {

                /**  缓存中不存在数据,则访问数据库
                 *  拿到数据锁, 在将数据从缓存中读取,
                 *  使用lua脚本
                 *  这里解释一下为什么是不使用 SETNX + EXPIRE 方式
                 *   因为:
                 *   setnx和expire两个命令分开了,「不是原子操作」。如果执行完setnx加锁,
                 *   正要执行expire设置过期时间时,进程crash或者要重启维护了。那么这个锁就永久,不会失效。
                 *  设置成功,返回 1 。 设置失败,返回 0
                 */
                if (tryLock(goodsId).equals(1)) {
                    Object dbObjet = selectDbById(goodsId);
                    setCache(goodsId, dbObjet);
                    unLock(goodsId);
                    return dbObjet;
                }
        }catch (Exception exception) {
            System.out.println("其他异常:" + exception.getMessage());
        }
        return null;
    }

KEYS[1] 为key ,ARGV[1] 为value , ARGV[2] 为 时间;

如果有多个参数, 也可用 hash 使用

redis_lock.lua

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
    return redis.call('expire',KEYS[1],ARGV[2])
else
    return 0
end

redis_unlock.lua

if redis.call("exists",KEYS[1]) == 0 then
    return 1
end
 
if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end

那么如果锁超时了,且业务代码并没有执行成功, 这会被其他重新设置?

续锁。 在启动个守护线程,如每2秒去 继续key 的有效期。 如果机器宕机了, 那就等过期时间失效。

如果定时器发现 key 还存在,则判断延长过期时间

redis_expire_key.lua

if redis.call("exists",KEYS[1]) == 1 then
     return redis.call('expire',KEYS[1],ARGV[2])
end
    return 0
end

缓存与数据库一致性问题

当数据库的数据发生修改时, 我们一般会时先修改数据库,还是缓存呢?比如数据库表中已经存在 id=10 ,name=张三的值;

当数据发生修改时,是先同步db, 后删除缓存?或者是先删除缓存在同步到db?

a.先同步db, 后删除缓存

2 个接口:

修改接口: 先修改db ,后再同步缓存到数据库

查询接口: 判断缓存是否存在, 不存在则直接查询缓存;
在这里插入图片描述

在并发情况下 , 是的数据库的数据和缓存不一致;

只是理论上会发生,概率很小,这问题的基础在于 缓存失效读+并发更新db;

b 先删除缓存,在更新db
在这里插入图片描述
同样的也会存在缓存和数据库不一直问题;

对于不一致的问题;

所有的写要都要以数据库为准,并所有的缓存都应该设置过期时间;

解决:

延迟双删策略(对于单个数据库来说, 他保证不了绝对的成功;)

1 先删除缓存,

2 在更新数据库

3 等待一段时间,在删除 (至于时间多久,则需要根据自己的的实际需求)

4 再次删除缓存

缺点:

1)延迟时间难以确认

到底是延迟一秒或者是几秒,这个其实很难确认,所以这个时间很难确定。

2)无法做到绝对的成功

即使延迟时间确认了,也根据上面的图,也会发现, 做不了100% 的成功;

3)读写分离或者一主多从

对于这样的数据机构,db binglog 的日志同步本身就是要花时间去同步,然而这个同步时间 没法去考量

如果db实现读写分离,或者读取的从db服务器为及时同步到 数据呢? 在从db查询不到数据的情况下,读取的值也是旧值,也有可能导致缓存和数据不一致的问题 。

那么就在于这个使用第二次删除的策略了,既要保证从db 能同步成功完成后再去删除缓存, 也要保缓存成功删除;

这个这样的问题,是不知道什么时候主从成功同步? 如果知道了,那么我们通知节点删除;那么是否可以监听一下binlog 日志呢?

binlog 日志

binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDL和DML等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等) 。它以事件形式记录并保存在二进制文件中。通过这些信息,我们可以再现数据更新操作的全过程。 mysql 主从之间的数据同步依靠异步的读取binlog 的方式;

最佳策列:

删除缓存+更新数据库+ cancal (监听binlog日志,删除缓存)

什么是Canal? 以及他能做什么?

Canal主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费。简单认为Canal就是一个简单的增量数据同步工具,能帮我们监听到数据的变化;

原理如下;

●canal模拟MySQL slave的交互协议,伪装自己为MySQL slave,向MySQL master发送dump协议。
●MySQL master收到dump请求,开始推送binary log给slave(即canal )。
●canal解析binary log对象(原始为byte流),再推送到MySQL、kafka、ElasticSearch等存储应用当中。
在这里插入图片描述
这种方案不适合频繁的修改。以及对一致性要求高的场景;当然这种方案还是有缺点的; 不过缺点相比其他还是可以接受的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值