Mysql/Redis缓存一致性

如何保证MySQL和Redis的缓存一致。从理论到实战。总结6种来感受一下。

理论知识

不好的方案

1.先写MySQL,再写Redis

图解说明:

这是一幅时序图,描述请求的先后调用顺序;

黄色的线是请求A,黑色的线是请求B;

黄色的文字是MySQL和Redis最终不一致的数据;

数据是从10更新为11;

后面的图为此规定

请求A、B都是先写MySQL,然后再写Redis,在高并发的情况下,如果请求A在写Redis时卡了一会,请求B已经一次完成了数据的更新,就会出现图中描述的问题。

图表述很清楚了,不过这里有个前提,就是对于读请求,先去读Redis,如果没有,再去读DB,但是读请求不会再写回Redis。就是读请求不会更新Redis。

2.先写Redis,再写MySQL

同1描述一样,秒懂。

3.先删除Redis,再写MySQL

和上面不一样的是,前面的请求A和B都是更新请求,这里的请求·A是跟新请求,但B请求是读请求,并且B的读请求会写回Redis。

请求A先删除缓存,可能因为卡顿,数据一直没有更新到MySQL,导致数据不一致。

这种情况出现的概率比较大,因为请求A更新MySQL可能会耗时比较长,而请求B的前两者都是查询,会比较快。

好的方案

4.先删除Redis,再写MySQL,再删除Redis

对于“先删除Redis,再写MySQL” ,如果要解决最后的不一致问题,其实再对Redis重新删除即可,这个就是“缓存双删”。

这个方案看看就行。

更好的方案是,异步串行化删除,即删除请求入队列

异步删除除对线上业务无影响,串行化处理保障并发情况下正确删除。

5.先写MySQL,再删除Redis

对于上面这种情况,对于第一次查询,请求B查询的数据10,但是MySQL的数据是11,只存在这一次不一致的情况,对于不是强一致的情况,对于不是强一致性要求的业务,可以容忍。对秒杀,库存就不行。

当请求B进行第二次查询时,因为没命中Redis,会重新擦汗一次DB,然后再回写到Redis。

这里需要满足两个条件:

        缓存刚好自动失效;

        请求B从数据库查10,回写缓存的消耗,比请求A写数据库,并且删除缓存的还长。

对于第二个条件,我们都知道更新DB肯定比查询耗时要长,所以出现这个情况的概率很小,同时满足上述条件情况更小。

6.先写MySQL,通过Binlog,异步更新Redis

这个方案,主要是监听MySQL的Binlog,然后通过异步的方式,将数据更新到Redis,这种方案有个前提,查询的请求,不会写回Redis。

这个方案,保证MySQL和Redis的最终一致性,但是如果中途请求B需要查询数据,如果缓存无数据,就直接查DB;如果缓存有数据,查询的数据也会存在不一致的情况。

所以这个方案,是实现最终一致性的终极方案,但是不能保证实时性。

几种方案比较

我们对比上述讨论的6种方案:‘

1.先写Redis,再写MySQL

这种方案,坑定是不会用,万一DB挂了,你把数据写到缓存,DB无数据,这个是灾难性的;

如果写DB失败,对Redis进行逆操作,那如果逆向操作失败,是不是得又搞个重试?

2.先写MySQL,再写Redis

对于并发量、一致性要求不高的项目,很多就是这么用的,我之前也经常这么搞

但是不建议这么做;

当Redis瞬间不可用的情况,需要报警出来,然后线下处理。

3.先删除Redis,再写MySQL

有懂得回答?

4.先删除Redis,再写MySQL,再删除Redis

这种方式虽然可行,但是感觉复杂,还要搞个消息队列去异步删除Redis。

5.先写MySQL,再删除Redis

比较推荐这总方案,删除Redis如果失败,可以再多重试几次,否则报警出来;

这个方案,是实时性最好的方案,在一些高并发场景种,推荐。

6.先写MySQL,通过Binlog。异步更新Redis

对于异地容灾,数据汇总,建议用这种,比如binlog+kafka,数据得一致性也可以达到秒级;

纯粹得高并发场景,不建议这种方案,入抢购,秒杀等。

个人结论:

实时性一致方案:采用“先写MySQL ,再删除Redis”的策略,这种情况下虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。

最终一致性方案:采用“先写MySQL,通过Binlog,异步更新Redis“,可以通过Binlog,结合消息队列异步更新Redis,是最终一致性的最优解。

项目实战

数据更新

因为项目对实时性要求高,所以采用方案5,先写MySQL,再删除Redis方式。

下面是一个示例,我们将文章的标签放入MySQL之后,在删除Redis,所有涉及到DB更新的操作都需要按照这种方式处理。

这里加了一个事务,如果Redis删除失败,MySQL的更新操作也要回滚,避免查询读取到脏数据。

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveTag(TagReq tagReq) {
        TagDO tagDO = ArticleConverter.toDO(tagReq);
        //先写MySQL
        if (NumUtil.nullOrZero(tagReq.getTagId())) {
            tagDao.save(tagDO);
        } else {
            tagDO.setId(tagReq.getTagId());
            tagDao.updateById(tagDO);
        }
        //再删除Redis
        String redisKey = CACHE_TAG_PRE + tagDO.getId();
        RedisClient.del(redisKey);
    }
    
    @Override
    @Transactional(rollbackFor = Excetion.class)
    public void deleteTag(Integer tagId) {
        TagDO tagDO = tagDao.getById(tagId);
        if (tagDO != null){
            //先写MySQL
            tagDao.removeById(tagId);
            //再删除Redis
            String redisKey = CACHE_TAG_PRE + tagDO.getId();
            RedisClien.del(redisKey);
        }
    }

    @Override
    public void operateTag(Integer tagId, Integer pushStatus) {
        TagDO tagDO = tagDao.getById(tagId);
        if (tagDO != null){
            //先写MySQL
            tagDO.setStatus(pushStatus);
            tagDao.updateById(tagDO);
            //再删除Redis
            String redisKey = CACHE_TAG_PRE + tagDO.getId();
            RedisClient.del(redisKey);
        }
    }

获取数据

也比较简单,先查缓存,如果有就直接返回;如果未查询到,需要先查询DB,再写入缓存。

我们放入缓存时,加了一个过期时间,用于兜底,万一两者不一致,缓存过期后,数据会重新更新到缓存。

    @Override
    public TagDTO getTagById(Long tagId) {

        String redisKey = CACHE_TAG_PRE + tagId;

        // 先查询缓存,如果有就直接返回
        String tagInfoStr = RedisClient.getStr(redisKey);
        if (tagInfoStr != null && !tagInfoStr.isEmpty()) {
            return JsonUtil.toObj(tagInfoStr, TagDTO.class);
        }

        // 如果未查询到,需要先查询 DB ,再写入缓存
        TagDTO tagDTO = tagDao.selectById(tagId);
        tagInfoStr = JsonUtil.toStr(tagDTO);
        RedisClient.setStrWithExpire(redisKey, tagInfoStr, CACHE_TAG_EXPRIE_TIME);

        return tagDTO;
    }

测试用例

@Slf4j
public class MysqlRedisService extends BasicTest {

    @Autowired
    private TagSettingService tagSettingService;

    @Test
    public void save() {
        TagReq tagReq = new TagReq();
        tagReq.setTag("Java");
        tagReq.setTagId(1L);
        tagSettingService.saveTag(tagReq);
        log.info("save success:{}", tagReq);
    }

    @Test
    public void query() {
        TagDTO tagDTO = tagSettingService.getTagById(1L);
        log.info("query tagInfo:{}", tagDTO);
    }
}

我们看一下Redis:

结果输出:

  • 34
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值