一、问题的引入

在redis面试的时候,一般会遇到如下问题:

 

  1. 只要使用了缓存,就会涉及到redis缓存和数据库双存储双写,只要是双写,就一定有数据一致性的问题,那么针对数据一致性的问题如何解决;
  2. 双写一致性在实施的时候是先动缓存,还是MySQL数据库,哪一个?
  3. 在生产中遇到这么一种情况,微服务查询redis没有数据,而mysql有数据,为了保证数据双写一致性回写redis你需要注意什么?
  4. 双检加锁策略你了解吗?如何避免缓存击穿?
  5. redis和MySQL双写100%会出问题,做不强一致性,如何保证最终一致性?

二、缓存双写一致性说明

1.如果redis中有数据,需要和数据库中的值相同

2.如果redis中无数据,数据库中的值要是最新值,且准备回写redis

3.缓存按照操作来分,分为两种:

3.1.只读缓存

3.2.读写缓存

(1).同步直写缓存:
  • 写数据库后也同步写入到redis缓存,缓存和数据库中的数据一致
  • 对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
(2).异步缓写策略
  • 正常业务运行中,MySQL数据变动了,但是可以在业务上容许出现一定时间后才作用于redis,例如:仓储物流
  • 异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者rebbitMQ等中间件,实现重试重写

4.双检加锁策略

4.1.业务问题,下面的操作怎么实现?

缓存双写一致性之更新策略_缓存

 

1.输入redis有数据,则直接从redis读取数据;
2.redis没有数据,则需要从MySQL获取数据;
3.步骤二从MySQL读取数据完成后,将MySQL数据回写进入redis;

4.2.双检加锁策略实现上述操作?

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

案例代码如下:下面的findUserById2方法就是采用的双检加锁

便笺
package com.augus.redis.service;

import com.augus.redis.entities.User;
import com.augus.redis.mapper.UserMapper;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class UserService {
    public static final String CACHE_KEY_USER = "user:";
    @Resource
    private UserMapper userMapper;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 对于公司(QPS《=1000)可以使用,但是大厂则是不行
     * @param id
     * @return
     */
    public User findUserById(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
        user = (User) redisTemplate.opsForValue().get(key);

        if(user == null)
        {
            //2 redis里面无,继续查询mysql
            user = userMapper.selectByPrimaryKey(id);
            if(user == null)
            {
                //3.1 redis+mysql 都无数据
                //这里需要在具体细化,防止多次穿透,业务规定,记录下导致穿透的这个key回写redis
                return user;
            }else{
                //3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
                redisTemplate.opsForValue().set(key,user);
            }
        }
        return user;
    }


    /**
     * 加强补充,避免突然key失效了,导致mysql宕机,做一下预防,尽量不出现击穿的情况。
     * @param id
     * @return
     */
    public User findUserById2(Integer id)
    {
        User user = null;
        String key = CACHE_KEY_USER+id;

        //1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,
        // 第1次查询redis,加锁前
        user = (User) redisTemplate.opsForValue().get(key);
        if(user == null) {
            //2对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
            synchronized (UserService.class){
                //第2次查询redis,加锁后
                user = (User) redisTemplate.opsForValue().get(key);
                //3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
                if (user == null) {
                    //4 查询mysql拿数据(mysql默认有数据)
                    user = userMapper.selectByPrimaryKey(id);
                    if (user == null) {
                        return null;
                    }else{
                        //5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
                        redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
                    }
                }
            }
        }
        return user;
    }

}

三、数据库和缓存一致性的更新策略

3.1.数据库和缓存一致性的更新策略的作用是什么?

目的在于保证数据的一致性,给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。

可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。

上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况。

3.2.数据更新的策略

 

 

四、redis和MySQL数据库实现双写一致性工程落地