[Notas de Redis] Penetración de caché y desglose de caché y contramedidas

1. Penetración de caché

1. El concepto de penetración de caché

La penetración de caché significa que al consultar datos que no existen , la caché (capa de almacenamiento en caché) no se verá afectada y se enviará una solicitud de consulta a la base de datos (capa de persistencia), pero tampoco se encontrarán los datos.

Puede ejercer presión sobre el rendimiento de la base de datos

2. Solución de penetración de caché

Cuando la capa de persistencia consulta datos vacíos que no existen, también se agregarán a la memoria caché y se establecerá el tiempo de caducidad.

Caché de datos vacíos para evitar una presión excesiva sobre la base de datos al buscar repetidamente datos vacíos

Las solicitudes para acceder a datos no válidos generalmente solo se accederán repetidamente en un período corto de tiempo (operación incorrecta o acceso malicioso), y se puede establecer un tiempo de vencimiento corto, como 5 minutos, para ahorrar espacio.

Código de muestra

Escenario: ver la información de un usuario según la identificación del usuario

Un ejemplo de la capa empresarial UserServiceImpl es el siguiente:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

    @Resource
    private StringRedisTemplate stringRedisTemplate;

	@Override
    public User getByIdTreatPenetration(Long id) {
    
    
        // 在缓存中查找用户
        String key = "cache:user:" + id;
        String userStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,命中则直接返回
        if (userStr != null) {
    
    
            if (userStr.equals(""))
                return null;
            else
                return JSONUtil.toBean(userStr, User.class);
        }
        // 缓存未命中,从数据库查找
        User user = this.getById(id);
        // 将数据存入缓存
        stringRedisTemplate.opsForValue().set(
                key
                , (user == null) ? "" : JSONUtil.toJsonStr(user)	// 空数据缓存空字符串
                , (user == null) ? 5 : 60	// 空数据缓存 5 分钟,非空缓存 60 分钟
                , TimeUnit.MINUTES
        );
        return user;
    }
}

2. Desglose de caché

1. El concepto de descomposición del caché

El desglose del caché significa que cuando el caché de ciertos datos calientes caduca , el servidor recibe una gran cantidad de solicitudes para consultar estos datos al mismo tiempo. En este momento, el nuevo caché aún no se ha establecido y una gran cantidad de consultas las solicitudes se enviarán a la base de datos.

2. Solución de descomposición de caché

Método 1: Bloqueo mutex

Adquiera el bloqueo cuando el caché se pierda, cree un nuevo caché y otros subprocesos esperen a que se libere el bloqueo antes de buscar datos

De esta manera, solo una solicitud de los mismos datos está accediendo a la base de datos al mismo tiempo.


[Características de bloqueo de exclusión mutua]

  • Mientras se construye el caché, otras solicitudes esperarán a que se complete la construcción, lo que puede ser más lento
  • Los datos encontrados por el usuario son estrictamente consistentes con la base de datos.

Código de muestra

También consulte la información de un usuario en función de la identificación del usuario

La operación de bloqueo utiliza Redisson. Sobre la base de la penetración de caché de procesamiento, es suficiente complementar la parte de bloqueo para la parte que pierde el caché.

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public User getByIdTreatBreakdownWithLock(Long id) {
    
    
        // 在缓存中查找用户
        String key = "user:" + id;
        String userStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,命中则直接返回
        if (userStr != null) {
    
    
            if (userStr.equals(""))
                return null;
            else
                return JSONUtil.toBean(userStr, User.class);
        }

        // 缓存未命中,获取锁,查询数据库并建立缓存
        // 获取锁 Redisson 对象
        String lockName = "lock:user" + id;
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.100.103:6379")
                .setPassword("123456")
                .setDatabase(0);
        RLock lock = Redisson.create(config).getLock(lockName);
        try {
    
    
            // 尝试获取锁
            boolean flag = lock.tryLock();
            if (!flag) {
    
    
                // 获取锁失败,锁已被占用,线程睡眠 50 毫秒,然后递归调用此方法查询数据
                Thread.sleep(50);
                return this.getByIdTreatBreakdownWithLock(id);
            }
            // 从数据库查询
            User user = this.getById(id);
            // 将数据存入缓存
            stringRedisTemplate.opsForValue().set(
                    key
                    , (user == null) ? "" : JSONUtil.toJsonStr(user)	// 空数据缓存空字符串
                    , (user == null) ? 5 : 60	// 空数据缓存 5 分钟,非空缓存 60 分钟
                    , TimeUnit.MINUTES
            );
            return user;
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }
    
}

Método 2: establezca el tiempo de caducidad lógico

Premisa: Únase al caché cuando se crean los datos y establezca el tiempo de caducidad lógico (el tiempo de caducidad real del caché es -1)

Cuando el caché falla, significa que los datos no existen en la base de datos y regresan vacíos.
Cuando el caché golpea, verifique si el tiempo de vencimiento lógico ha expirado. Si ha expirado, devuelva los datos expirados y restablezca el caché en un nuevo subproceso y establezca el tiempo de caducidad lógico.


[Características de caducidad lógica]

  • Cuando se crean los datos, se deben agregar a la memoria caché.
  • Cree un caché de forma asíncrona sin afectar otras solicitudes, y la velocidad es más rápida
  • Dado que la caducidad lógica aún devuelve datos caducados, los datos encontrados por el usuario pueden ser coherentes con la base de datos.

Código de muestra

Consultar la información de un usuario según el id de usuario

Dado que es necesario configurar el tiempo de caducidad lógico, los datos reales y el tiempo de caducidad se pueden empaquetar en una clase, un ejemplo es el siguiente:

@Data
public RedisData getRedisData(String key) {
    
    
    String jsonStr = get(key);
    if (jsonStr == null) {
    
    
        return null;
    }
    return JSONUtil.toBean(jsonStr, RedisData.class);
}

Un ejemplo de la capa empresarial es el siguiente:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

	// 线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    @Override
    public User getByIdTreatBreakdownWithLogicExpire(Long id) {
    
    
        String key = "user:" + id;
        // 查询缓存
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 缓存未命中,即代表数据不存在,返回空
		if (redisData == null) {
    
    
            return null;
        }
        // 将缓存字符串装换为对象
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        User user = jsonObject.toBean(User.class);
        LocalDateTime expirationTime = redisData.getExpirationTime();
        // 未到期,直接返回
        if (LocalDateTime.now().isBefore(expirationTime)) {
    
    
            return user;
        }
        
        // 已到期,重建缓存
        String lockName = "lock:user:" + id;
        RLock lock = redissonClient.getLock(lockName);
        boolean flag = lock.tryLock();
        if (flag) {
    
    
	        // 获取锁后进行二次判断,现有缓存是否过期
            jsonStr = stringRedisTemplate.opsForValue().get(key);
            redisData = JSONUtil.toBean(jsonStr, StringRedisCacheUtil.RedisData.class);
            expirationTime = redisData.getExpirationTime();
            // 第二次判断未到期,说明缓存已被别的线程重建,返回结果
            if (LocalDateTime.now().isBefore(expirationTime)) {
    
    
                return user;
            }
			// 新线程中重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
                User user1 = this.getById(id);
                StringRedisCacheUtil.RedisData redisData1 = new StringRedisCacheUtil.RedisData();
                redisData1.setData(user1);
                redisData1.setExpirationTime(LocalDateTime.now().plusMinutes(60));
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData1));
                lock.unlock();
            });

        }
        return user;

    }
}

3. Ejemplos de herramientas

@Component
public class StringRedisCacheUtil {
    
    

    private static StringRedisTemplate stringRedisTemplate;
    private static RedissonClient redissonClient;

    private static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Data
    public static class RedisData {
    
    
        private Object data;
        private LocalDateTime expirationTime;
    }

    private static Long NULL_CACHE_TTL = 5L;
    private static TimeUnit NULL_CACHE_TIMEUNIT = TimeUnit.MINUTES;

    // Spring 容器启动时,为静态变量注入对象
    // ----------------------------------------------------
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient client;
    @PostConstruct
    private void init() {
    
    
        stringRedisTemplate = this.redisTemplate;
        redissonClient = client;
    }
    private StringRedisCacheUtil() {
    
     }
    // ----------------------------------------------------

    // 根据键获取值
    public static String get(String key) {
    
    
        return stringRedisTemplate.opsForValue().get(key);
    }

    // 根据键获取带逻辑时间的值
    public static RedisData getRedisData(String key) {
    
    
        String jsonStr = get(key);
        if (jsonStr == null) {
    
    
            return null;
        }
        return JSONUtil.toBean(jsonStr, RedisData.class);
    }

    // 判断是否超过逻辑过期时间
    public static boolean isExpired(RedisData redisData) {
    
    
        return redisData == null || LocalDateTime.now().isAfter(redisData.getExpirationTime());
    }


    // 判断是否还没有逻辑过期时间
    public static boolean isNotExpired(RedisData redisData) {
    
    
        return !isExpired(redisData);
    }


    // 设置缓存
    public static void set(String key, String value, Long timeout, TimeUnit unit) {
    
    
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    // 为空数据设置缓存
    public static void setNull(String key) {
    
    
        set(key, "", NULL_CACHE_TTL, NULL_CACHE_TIMEUNIT);
    }

    // 设置缓存并带有逻辑过期时间
    public static void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) {
    
    
        RedisData redisData = new RedisData();
        redisData.setData(value);
        LocalDateTime now = LocalDateTime.now();
        redisData.setExpirationTime(now.plusMinutes(unit.toSeconds(timeout)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据 id 查询数据,并解决缓存穿透问题(不解决击穿问题)
     *
     * @param keyPrefix      键前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        过期时间
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据或 null
     */
    public static <R, ID> R getByIdTreatPenetration(String keyPrefix, ID id, Class<R> resultType,
                                                    Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id.toString();
        String JSONStr = get(key);
        if (JSONStr != null) {
    
    
            if (JSONStr.equals("")) return null;
            return JSONUtil.toBean(JSONStr, resultType);
        }
        R result = dbCallbackFunc.apply(id);
        if (result == null) setNull(key);
        else set(key, JSONUtil.toJsonStr(result), timeOut, unit);
        return result;
    }

    /**
     * 根据 id 查询数据,利用互斥锁解决缓存击穿问题
     *
     * @param keyPrefix      键前缀
     * @param lockNamePrefix 锁名称前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        过期寿命
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据或 null
     */
    public static <R, ID> R getByIdTreatBreakdownWithLock(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,
                                                          Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id.toString();
        String JSONStr = get(key);
        if (JSONStr != null) {
    
    
            if (JSONStr.equals("")) return null;
            return JSONUtil.toBean(JSONStr, resultType);
        }

        String lockName = lockNamePrefix + id;
        RLock lock = redissonClient.getLock(lockName);
        try {
    
    
            boolean flag = lock.tryLock();
            if (!flag) {
    
    
                Thread.sleep(50);
                return getByIdTreatBreakdownWithLock(keyPrefix, lockNamePrefix, id, resultType, dbCallbackFunc, timeOut, unit);
            }
            R result = dbCallbackFunc.apply(id);
            if (result == null) setNull(key);
            else set(key, JSONUtil.toJsonStr(result), timeOut, unit);
            return result;
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 根据 id 查询数据,利用逻辑过期解决缓存击穿问题(缓存未命中则代表不存在数据,不考虑穿透问题)
     *
     * @param keyPrefix      键前缀
     * @param lockNamePrefix 锁名称前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        逻辑过期寿命
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据
     */
    public static <R, ID> R getByIdTreatBreakdownWithLogicExpire(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,
                                                                 Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id;
        RedisData redisData = getRedisData(key);

        if (redisData == null) {
    
    
            return null;
        }

        JSONObject jsonObject = (JSONObject) redisData.getData();
        R result = JSONUtil.toBean(jsonObject, resultType);

        if (isNotExpired(redisData)) {
    
    
            return result;
        }

        String lockName = lockNamePrefix + id.toString();
        RLock lock = redissonClient.getLock(lockName);

        boolean flag = lock.tryLock();
        if (flag && isExpired(getRedisData(key))) {
    
    
            CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
                R data = dbCallbackFunc.apply(id);
                setWithLogicalExpire(key, data, timeOut, unit);
                lock.unlock();
            });
        }
        return result;
    }

}

Supongo que te gusta

Origin blog.csdn.net/Cey_Tao/article/details/127454635
Recomendado
Clasificación