[Función empresarial 88] Microservicio-springcloud-bloqueo distribuido-redis-redisson-bloqueo reentrante-lectura-escritura bloqueo-enganche-semáforo-caché problema de coherencia de datos-modo de doble escritura-modo de falla-springcache

2. Cerradura distribuida

1. El principio del bloqueo distribuido.

  La esencia de los bloqueos distribuidos o los bloqueos locales es en realidad la misma: ambos convierten operaciones paralelas en operaciones en serie.

imagen.png

2. Soluciones comunes para cerraduras distribuidas.

2.1 Base de datos

可以利用MySQL隔离性:唯一索引
use test;
CREATE TABLE `DistributedLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_name` (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性

lock(name,desc){
    insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}

unlock(name){
    delete from DistributedLock where name = #{name}
}

Se pueden usar bloqueos exclusivos para implementar seleccionar... dónde... para actualizar;

Bloqueo optimista: crea con optimismo que no habrá problemas de seguridad en los datos y, si es así, inténtelo de nuevo.

select ...,version;
update table set version+1 where version = xxx

2.2 Redis

setNX: setNX (clave, valor): si la clave no existe, agregue el valor de la clave; de ​​lo contrario, la suma falla, Redisson

2.3Cuidador del zoológico

imagen.png

3.Redis implementa bloqueos distribuidos

  En Redis, la preferencia de bloqueo se logra mediante el comando setNX. El código básico para usar este comando para implementar bloqueos distribuidos es:

    public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
    
    
        String keys = "catalogJSON";
        // 加锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111");
        if(lock){
    
    
            // 加锁成功
            Map<String, List<Catalog2VO>> data = getDataForDB(keys);
            // 从数据库中获取数据成功后,我们应该要释放锁
            stringRedisTemplate.delete("lock");
            return data;
        }else{
    
    
            // 加锁失败
            // 休眠+重试
            // Thread.sleep(1000);
            return getCatelog2JSONDbWithRedisLock();
        }
    }

  El código anterior en realidad tiene algunos problemas. En primer lugar, si se produce una excepción en el método getDataForDB(keys), entonces no eliminaremos la clave y no liberaremos el bloqueo, lo que provocará un punto muerto. Para solucionar este problema, podemos Esto se soluciona estableciendo el tiempo de vencimiento, el código específico es el siguiente:

    public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
    
    
        String keys = "catalogJSON";
        // 加锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111");
        if(lock){
    
    
            // 给对应的key设置过期时间
            stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
            // 加锁成功
            Map<String, List<Catalog2VO>> data = getDataForDB(keys);
            // 从数据库中获取数据成功后,我们应该要释放锁
            stringRedisTemplate.delete("lock");
            return data;
        }else{
    
    
            // 加锁失败
            // 休眠+重试
            // Thread.sleep(1000);
            return getCatelog2JSONDbWithRedisLock();
        }
    }

  Aunque lo anterior resuelve el problema de las excepciones en el método getDataForDB, ¿qué pasa si se interrumpe antes de que se ejecute el método de caducidad? Esto también causará el problema de punto muerto que presentamos, entonces, ¿qué debemos hacer al respecto? En este momento, esperamos que las operaciones de setNx y la configuración del tiempo de vencimiento puedan garantizar la atomicidad.

En este momento, también podemos especificar el tiempo de vencimiento en el método setIfAbsent para garantizar este comportamiento atómico.

    public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
    
    
        String keys = "catalogJSON";
        // 加锁 在执行插入操作的同时设置了过期时间
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",30,TimeUnit.SECONDS);
        if(lock){
    
    
            // 给对应的key设置过期时间
            stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
            // 加锁成功
            Map<String, List<Catalog2VO>> data = getDataForDB(keys);
            // 从数据库中获取数据成功后,我们应该要释放锁
            stringRedisTemplate.delete("lock");
            return data;
        }else{
    
    
            // 加锁失败
            // 休眠+重试
            // Thread.sleep(1000);
            return getCatelog2JSONDbWithRedisLock();
        }
    }

  Si el tiempo de ejecución del negocio para obtener el bloqueo es relativamente largo y excede el tiempo de vencimiento que establecimos, entonces es posible que el bloqueo se libere antes de que se complete el negocio, y luego llega otra solicitud y crea la clave. En este momento, el negocio original Una vez completado el procesamiento, cuando elimina la clave, es posible eliminar la clave de otra persona. ¿Qué debe hacer en este caso? En este caso, la información de bloqueo que podemos consultar se distingue por UUID. El código específico es el siguiente:

public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
    
    
        String keys = "catalogJSON";
        // 加锁 在执行插入操作的同时设置了过期时间
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
        if(lock){
    
    
            // 给对应的key设置过期时间
            stringRedisTemplate.expire("lock",20,TimeUnit.SECONDS);
            // 加锁成功
            Map<String, List<Catalog2VO>> data = getDataForDB(keys);
            // 获取当前key对应的值
            String val = stringRedisTemplate.opsForValue().get("lock");
            if(uuid.equals(val)){
    
    
                // 说明这把锁是自己的
                // 从数据库中获取数据成功后,我们应该要释放锁
                stringRedisTemplate.delete("lock");
            }
            return data;
        }else{
    
    
            // 加锁失败
            // 休眠+重试
            // Thread.sleep(1000);
            return getCatelog2JSONDbWithRedisLock();
        }
    }

  Consultar el valor de la clave y eliminar la clave anterior no son en realidad operaciones atómicas. Esto hará que el tiempo expire después de consultar la clave y luego la clave se elimine. Luego, otras solicitudes crean una nueva clave y luego la ejecución original. Después de eliminar esta clave, ocurrió otra situación en la que se eliminaron las claves de otras personas. En este momento debemos asegurarnos de que la consulta y la eliminación sean un comportamiento atómico.

public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisLock() {
    
    
        String keys = "catalogJSON";
        // 加锁 在执行插入操作的同时设置了过期时间
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if(lock){
    
    
            Map<String, List<Catalog2VO>> data = null;
            try {
    
    
                // 加锁成功
                data = getDataForDB(keys);
            }finally {
    
    
                String srcipts = "if redis.call('get',KEYS[1]) == ARGV[1]  then return redis.call('del',KEYS[1]) else  return 0 end ";
                // 通过Redis的lua脚本实现 查询和删除操作的原子性
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(srcipts,Long.class)
                        ,Arrays.asList("lock"),uuid);
            }
            return data;
        }else{
    
    
            // 加锁失败
            // 休眠+重试
            // Thread.sleep(1000);
            return getCatelog2JSONDbWithRedisLock();
        }
    }

https://space.bilibili.com/435498550 Implementación de bloqueos distribuidos

4.Bloqueo distribuido Redisson

4.1 Integración de Redisson

Añade las dependencias correspondientes.

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.1</version>
        </dependency>

Agregue la clase de configuración correspondiente

@Configuration
public class MyRedisConfig {
    
    

    @Bean
    public RedissonClient redissonClient(){
    
    
        Config config = new Config();
        // 配置连接的信息
        config.useSingleServer()
                .setAddress("redis://192.168.56.100:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return  redissonClient;
    }
}

4.2 Bloqueo reentrante

/**
     * 1.锁会自动续期,如果业务时间超长,运行期间Redisson会自动给锁重新添加30s,不用担心业务时间,锁自动过期而造成的数据安全问题
     * 2.加锁的业务只要执行完成, 那么就不会给当前的锁续期,即使我们不去主动的释放锁,锁在默认30s之后也会自动的删除
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
    
    
        RLock myLock = redissonClient.getLock("myLock");
        // 加锁
        myLock.lock();
        try {
    
    
            System.out.println("加锁成功...业务处理....." + Thread.currentThread().getName());
            Thread.sleep(30000);
        }catch (Exception e){
    
    

        }finally {
    
    
            System.out.println("释放锁成功..." +  Thread.currentThread().getName());
            // 释放锁
            myLock.unlock();
        }
        return "hello";
    }

4.3 Bloqueo de lectura-escritura

  Según las operaciones comerciales, podemos dividirlas en operaciones de lectura y escritura. Las operaciones de lectura en realidad no afectarán los datos. Si también realizamos procesamiento en serie en las operaciones de lectura, la eficiencia será muy baja. En este momento, podemos resolver esto. problema a través de bloqueos de lectura y escritura.

@GetMapping("/writer")
    @ResponseBody
    public String writerValue(){
    
    
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
        // 加写锁
        RLock rLock = readWriteLock.writeLock();
        String s = null;
        rLock.lock(); // 加写锁
        try {
    
    
            s = UUID.randomUUID().toString();
            stringRedisTemplate.opsForValue().set("msg",s);
            Thread.sleep(30000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            rLock.unlock();
        }
        return s;
    }

    @GetMapping("/reader")
    @ResponseBody
    public String readValue(){
    
    
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
        // 加读锁
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        String s = null;
        try {
    
    
            s = stringRedisTemplate.opsForValue().get("msg");
        }finally {
    
    
            rLock.unlock();
        }

        return s;
    }

En un bloqueo de lectura-escritura, solo el comportamiento de lectura y lectura es un bloqueo compartido, que no se afecta entre sí, siempre que exista un comportamiento de escritura, es un bloqueo de exclusión mutua (bloqueo exclusivo).

4.4 Bloqueo

El objeto Java de bloqueo distribuido de Redisson ( CountDownLatchRCountDownLatch ) basado en Redisson adopta java.util.concurrent.CountDownLatchuna interfaz y un uso similares.

@GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor(){
    
    
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.trySetCount(5);
        try {
    
    
            door.await(); // 等待数量降低到0
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        return "关门熄灯...";
    }

    @GetMapping("/goHome/{id}")
    @ResponseBody
    public String goHome(@PathVariable Long id){
    
    
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown(); // 递减的操作
        return id + "下班走人";
    }

4.5 Semáforo

Semáforo distribuido ( Semaphore ) Objeto Java basado en Redis de Redisson

RSemaphoreAdopta java.util.concurrent.Semaphoreuna interfaz y un uso similares. También proporciona interfaces estándar asincrónicas (Async) , reflexivas (Reactive) y RxJava2.

@GetMapping("/park")
    @ResponseBody
    public String park(){
    
    
        RSemaphore park = redissonClient.getSemaphore("park");
        boolean b = true;
        try {
    
    
            // park.acquire(); // 获取信号 阻塞到获取成功
            b = park.tryAcquire();// 返回获取成功还是失败
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return "停车是否成功:" + b;
    }

    @GetMapping("/release")
    @ResponseBody
    public String release(){
    
    
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();
        return "释放了一个车位";
    }

4.6 Problema de coherencia de los datos de la caché

imagen.png

imagen.png

imagen.png

imagen.png

¿Cómo elegimos entre las dos soluciones anteriores?

  1. Agregamos un tiempo de vencimiento a todos los datos almacenados en caché y activamos activamente operaciones de actualización después de que caduquen los datos.
  2. Utilice bloqueos de lectura y escritura para procesar, y las operaciones de lectura y lectura no se afectan entre sí.

Ya sea en modo de doble escritura o en modo de falla, causará problemas de inconsistencia de caché. Es decir, algo sucederá si se actualizan varias instancias al mismo tiempo. ¿qué hacer?

  1. Si se trata de datos de latitud del usuario (datos de pedido, datos de usuario), la probabilidad de concurrencia es muy pequeña. No es necesario considerar este problema. Los datos almacenados en caché más el tiempo de vencimiento pueden desencadenar actualizaciones activas de lecturas de vez en cuando
    . .
  2. Si se trata de datos básicos como menús y presentaciones de productos, también puede utilizar el canal para suscribirse a binlog.
  3. El almacenamiento en caché de datos + tiempo de vencimiento también es suficiente para resolver la mayoría de los requisitos comerciales de almacenamiento en caché.
  4. La lectura y escritura simultáneas se garantizan mediante el bloqueo, y la escritura y la escritura se ponen en cola en orden. No importa si lees o lees. Por lo tanto, es adecuado utilizar el bloqueo de lectura y escritura. (A la empresa no le importan los
    datos sucios y permite que se ignoren los datos sucios temporales)

Resumir:

  • Los datos que podemos colocar en la caché no deberían tener requisitos de coherencia y tiempo real extremadamente altos. Por lo tanto, al almacenar datos en caché, agregue el tiempo de vencimiento para asegurarse de obtener los datos más recientes todos los días.
  • No debemos diseñar demasiado ni aumentar la complejidad del sistema.
  • Cuando encuentre datos con altos requisitos de consistencia y tiempo real, debe verificar la base de datos, incluso si es más lenta.

3. Caché de primavera

Dependencia

        <!-- 导入SpringCache的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

clase de configuración

  • Especifique un método de serialización personalizado. El formato de valor en el cliente Redis es el formato json, que es más conveniente.
  • Para establecer el tiempo de vencimiento, además de configurar spring.cache.redis.time-to-live en el archivo de configuración yaml para especificar el tiempo de vencimiento, también debe llamar al método correspondiente en el archivo de configuración para obtener el tiempo de vencimiento configurado. .
  • Configure para evitar la penetración, es decir, almacenar en caché los valores nulos para evitar que una gran cantidad de consultas de valores nulos pasen por alto redis, cache-null-values: true # Si se deben almacenar en caché los valores nulos para evitar la penetración de la caché, también es necesario un archivo de configuración para llamar al método correspondiente para implementarlo.
  • Establezca el prefijo de clave, especifique el prefijo key-prefix: pro_; use-key-prefix: true (verdadero por defecto) también necesita llamar al método correspondiente en la clase de configuración para implementar

clase de inicio

  • Agregue la anotación **@EnableCaching** a la clase de inicio para habilitar el almacenamiento en caché
@Configuration
public class MyCacheConfig {
    
    

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
    
    
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 指定自定义的序列化的方式
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
    
    
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
    
    
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
    
    
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
    
    
            config = config.disableKeyPrefix();
        }

        return config;
    }
}

aplicación.yml

spring:
	 redis:
	   host: 192.168.56.100
	   port: 6379
	 cache:
	   type: redis # SpringCache 缓存的类型是 Redis
	   redis:
	     time-to-live: 60000 # 指定缓存key的过期时间
	     # key-prefix: bobo_
	     use-key-prefix: true
	     cache-null-values: true # 是否缓存空值,防止缓存穿透

Ejemplo de uso

    /**
     * 查询出所有的商品大类(一级分类)
     *    在注解中我们可以指定对应的缓存的名称,起到一个分区的作用,一般按照业务来区分
     *    @Cacheable({"catagory","product"}) 代表当前的方法的返回结果是需要缓存的,
     *                                       调用该方法的时候,如果缓存中有数据,那么该方法就不会执行,
     *                                       如果缓存中没有数据,那么就执行该方法并且把查询的结果缓存起来
     *    缓存处理
     *       1.存储在Redis中的缓存数据的Key是默认生成的:缓存名称::SimpleKey[]
     *       2.默认缓存的数据的过期时间是-1永久
     *       3.缓存的数据,默认使用的是jdk的序列化机制
     *    改进:
     *       1.生成的缓存数据我们需要指定自定义的key: key属性来指定,可以直接字符串定义也可以通过SPEL表达式处理:#root.method.name
     *       2.指定缓存数据的存活时间: spring.cache.redis.time-to-live 指定过期时间
     *       3.把缓存的数据保存为JSON数据
     *   SpringCache的原理
     *     CacheAutoConfiguration--》根据指定的spring.cache.type=reids会导入 RedisCacheAutoConfiguration
     * @return
     */
    @Trace
    @Cacheable(value = {
    
    "catagory"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLeve1Category() {
    
    
        System.out.println("查询了数据库操作....");
        long start = System.currentTimeMillis();
        List<CategoryEntity> list = baseMapper.queryLeve1Category();
        System.out.println("查询消耗的时间:" + (System.currentTimeMillis() - start));
        return list;
    }

SpringCache insuficiente:

1).Modo de lectura

  • Penetración de caché: consulta de datos nulos. Se puede resolver cache-null-values=true
  • Desglose de la caché: una gran cantidad de consultas simultáneas llegan al mismo tiempo para consultar datos que están caducados. Solución: sincronización de bloqueo distribuido = bloqueo local verdadero
  • Avalancha de caché: una gran cantidad de claves caducan al mismo tiempo. Solución: agregue el tiempo de vencimiento time-to-live=60000 para especificar el tiempo de vencimiento

2).Modo de escritura

  • bloqueo de lectura-escritura
  • Introduzca el canal y supervise los archivos de registro binlog para actualizar los datos de forma sincrónica
  • Lea más y escriba más, simplemente vaya directamente a la base de datos para leer los datos

Resumir:

  • Datos regulares (leer más y escribir menos): y si los requisitos de puntualidad y coherencia de los datos no son altos, podemos usar SpringCache por completo
  • Circunstancias especiales: Las situaciones especiales se manejan de manera especial.

Supongo que te gusta

Origin blog.csdn.net/studyday1/article/details/132576571
Recomendado
Clasificación