[Redis] Resuelva el problema de la descomposición de la memoria caché a partir de dos aspectos del bloqueo de exclusión mutua y la caducidad lógica

prefacio

En tiempos difíciles, prepárate para un día lluvioso

1. ¿Qué es el desglose de caché?

Para decirlo sin rodeos, una clave que se usa con mucha frecuencia falla repentinamente y la solicitud pierde el caché, lo que hace que innumerables solicitudes caigan en la base de datos, arrastrando la base de datos en un instante. ¡Y esa tecla también se llama tecla de acceso rápido!

inserte la descripción de la imagen aquí
Se puede ver intuitivamente que si desea resolver la falla del caché, no debe permitir que tantos hilos de solicitudes accedan a la base de datos en un período de tiempo determinado.
En base a esto, existen dos soluciones para restringir el acceso a la base de datos:

2. Resuelva el desglose de caché basado en mutex

Para una interfaz de consulta de id con acceso frecuente, la caché puede fallar. A continuación, se usa un mutex para resolver el problema.
inserte la descripción de la imagen aquí
En el pasado, la interfaz de información de consulta de id generalmente escribía la información consultada en la caché. Realice el tratamiento correspondiente. En el caso de la concurrencia, cuando la tecla de acceso rápido falla, una gran cantidad de solicitudes llegarán directamente a la base de datos e intentarán reconstruir la memoria caché, lo que probablemente detenga la base de datos y provoque la interrupción del servicio. Para tal situación, cuando se pierde el caché, el mejor punto de procesamiento es el paso después de juzgar si el caché se encuentra en el negocio, es decir, si la solicitud "extra" accede a la base de datos o no.
¿Las solicitudes de otros subprocesos pueden acceder a la base de datos? ¿Cuándo puedo acceder a la base de datos?
¿Pueden otros subprocesos acceder a la base de datos? ——Bloqueo,
¿cuando solo puedes tener un candado para acceder a la base de datos? —— Espere a que el subproceso principal libere el bloqueo
, ¿qué deben hacer otros subprocesos cuando no pueden obtener el bloqueo? ——Vete a dormir, vuelve más tarde

Para darnos cuenta de que solo un subproceso puede adquirir el bloqueo cuando varios subprocesos están en paralelo, podemos usar el setnx que viene con Redis
inserte la descripción de la imagen aquí
para garantizar que la operación de escritura se pueda realizar cuando la clave no existe y la operación de escritura no se puede realizar. se realiza cuando existe la clave. Garantiza perfectamente que solo el primer subproceso en obtener el bloqueo puede escribir en condiciones concurrentes, y después de que termine de escribir (sin liberarlo), otros no podrán escribir.
¿Cómo conseguirlo? Escriba un valor clave,
¿cómo liberarlo? Eliminar la clave de la cerradura (generalmente establece un período de validez para evitar la situación de no liberar durante mucho tiempo)

De esta forma, puedo encapsular dos métodos basados ​​en esta condición, uno escribe la clave para intentar adquirir el candado y el otro borra la clave para liberar el candado. Me gusta esto:

/**
 * 尝试获取锁
 *
 * @param key
 * @return
 */
private boolean tryLock(String key) {
    
    
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

/**
 * 释放锁
 *
 * @param key
 */
private void unlock(String key) {
    
    
    stringRedisTemplate.delete(key);
}

Paralelamente, cada vez que otros subprocesos deseen adquirir el bloqueo, deben escribir su propia clave en el método tryLock() para acceder al caché. Si setIfAbsent() devuelve falso, significa que un subproceso está actualizando los datos del caché y el bloqueo ha no ha sido liberado. Si devuelve verdadero, significa que el subproceso actual obtuvo el bloqueo y puede acceder al caché o incluso operar el caché.
En el siguiente escenario de consulta popular, usamos código para implementar mutex para resolver el desglose de caché
inserte la descripción de la imagen aquí

    /**
     * 解决缓存击穿的互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
    
    
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
    
     //不为空就返回 此工具类API会判断""为false
            //存在则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            //return Result.ok(shop);
            return shop;
        }
        //3.判断是否为空值
        if (shopJson != null) {
    
    
            //返回一个空值
            return null;
        }
        //4.缓存重建
        //4.1获得互斥锁
        String lockKey = "lock:shop"+id;
        Shop shopById=null;
        try {
    
    
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if (!isLock){
    
    
                //4.3失败,则休眠并重试
                Thread.sleep(50);
               return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shopById = getById(id);
            //5.不存在则返回错误
            if (shopById == null) {
    
    
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //return Result.fail("暂无该商铺信息");
                return null;
            }
            //6.存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            //7.释放互斥锁
            unlock(lockKey);
        }

        return shopById;
    }

3. Resuelva el desglose de la memoria caché en función de la caducidad lógica

La caducidad lógica no es una caducidad real. No necesitamos configurar TTL para la clave correspondiente, pero usamos la lógica comercial para lograr un efecto similar a la "caducidad". ¡Su esencia es limitar la cantidad de solicitudes que caen en la base de datos! Pero la premisa es sacrificar la consistencia para garantizar la disponibilidad, o la interfaz del negocio anterior, y resolver la falla de la caché utilizando la caducidad lógica:
inserte la descripción de la imagen aquí
de esta manera, la caché básicamente se verá afectada, porque no configuré ningún tiempo de caducidad para el caché, y para El conjunto de claves está todo seleccionado de antemano.Si hay una falta, básicamente se puede juzgar que él no está en la selección, por lo que puedo devolver directamente un mensaje de error. En el caso de un acierto, primero es necesario determinar si el tiempo lógico ha expirado y luego decidir si reconstruir la memoria caché según el resultado. Y el tiempo lógico aquí es reducir una gran cantidad de solicitudes a una "puerta de enlace" que recae en la base de datos.

Después de leer el párrafo anterior, creo que todos todavía están confundidos. Dado que no hay un tiempo de caducidad establecido, ¿por qué todavía necesita juzgar el tiempo de caducidad lógico? ¿Por qué todavía hay un problema de si ha caducado?
De hecho, el llamado tiempo de caducidad lógico aquí es solo un campo de atributo de una clase. No se ha elevado a Redis en absoluto, sino al nivel de caché, que se utiliza para ayudar a juzgar el objeto de consulta. Es decir , el llamado tiempo de caducidad está separado de los datos almacenados en caché. Sí, por lo que no hay ningún problema de caducidad de caché y, naturalmente, la base de datos no estará bajo presión.

etapa de código:

Para ajustarse al principio de apertura y cierre tanto como sea posible, los atributos de la entidad original no se extienden por herencia, sino por combinación.

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;  //这里用Object是因为以后可能还要缓存别的数据
}

Encapsule un método para simular el tiempo de caducidad de la lógica de actualización y los datos almacenados en caché para ejecutar en la clase de prueba para lograr el efecto de los datos y el calor.

/**
 * 添加逻辑过期时间
 *
 * @param id
 * @param expireTime
 */
public void saveShopRedis(Long id, Long expireTime) {
    
    
    //查询店铺信息
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
    //将封装过期时间和商铺数据的对象写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

Interfaz de consulta:

/**
 * 逻辑过期解决缓存击穿
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
    
    
    String key = CACHE_SHOP_KEY + id;
    Thread.sleep(200);
    //1.从Redis查询缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
    
    
        //不存在则直接返回
        return null;
    }
    //3.判断是否为空值
    if (shopJson != null) {
    
    
        //返回一个空值
        //return Result.fail("店铺不存在!");
        return null;
    }
    //4.命中
    //4.1将JSON反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4.2判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
    
    
        //5.未过期则返回店铺信息
        return shop;
    }
    //6.过期则缓存重建
    //6.1获取互斥锁
    String LockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(LockKey);
    //6.2判断是否成功获得锁
    if (isLock) {
    
    
        //6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
            try {
    
    
                //重建缓存
                this.saveShop2Redis(id, 20L);

            } catch (Exception e) {
    
    
                throw new RuntimeException(e);
            } finally {
    
    
                //释放锁
                unlock(LockKey);
            }
        });
    }
    //6.4返回商铺信息
    return shop;
}

4. Prueba de interfaz

Se puede ver que la prueba de la interfaz se realiza simulando escenarios concurrentes a través de APIfox, y el tiempo promedio de consumo es aún muy corto, y el registro de la consola no tiene acceso frecuente a la base de datos:
inserte la descripción de la imagen aquí
ya que ApiFox no admite una gran cantidad de subprocesos, usé jmeter para probar con 1550 subprocesos Después de un tiempo, ¡la interfaz aún puede ejecutarse!
inserte la descripción de la imagen aquí
Parece que el rendimiento de la interfaz no es malo en escenarios concurrentes, y el QPS también es bastante ideal

5. Comparación entre ambos

Se puede ver que el nivel de código del método mutex es más simple, y solo es necesario encapsular dos métodos simples para operar el bloqueo. El método de caducidad lógica es más complicado y es necesario agregar clases de entidad adicionales.Después de encapsular el método, es necesario simular el calentamiento de datos en la clase de prueba.
Por el contrario, el primero no consume memoria adicional (no abre nuevos subprocesos) y tiene una sólida coherencia de datos, pero los subprocesos deben esperar, el rendimiento puede ser deficiente y existe el riesgo de interbloqueo. Este último abre un nuevo hilo con consumo de memoria adicional, sacrificando la consistencia para garantizar la disponibilidad, pero el rendimiento es mejor sin esperar.

Supongo que te gusta

Origin blog.csdn.net/weixin_57535055/article/details/128572301
Recomendado
Clasificación