Serie de procedimientos Cómo garantizar la seguridad de subprocesos de código en entornos autónomos y de clúster


En la programación de subprocesos múltiples, la seguridad de subprocesos es un concepto clave, que implica corrección y consistencia cuando varios subprocesos acceden y modifican recursos compartidos. Ya sea en un entorno independiente o en un entorno de clúster distribuido, garantizar la seguridad de subprocesos es un problema al que los desarrolladores deben prestar atención.

imagen

¿Qué es la seguridad de subprocesos?

La seguridad de subprocesos es un término en el diseño de programación, lo que significa que cuando se llama a una función o biblioteca de funciones en un entorno de subprocesos múltiples , puede manejar correctamente las variables comunes entre subprocesos múltiples , para que la función del programa se pueda completar correctamente .

Lo anterior es de Wikipedia.

Proporcione un ejemplo para ilustrar la importancia de la seguridad de subprocesos. Supongamos que hay una sala de cine que proyecta una película con un total de 10 asientos. Si no hay medidas de protección seguras contra subprocesos, cuando varias personas se apresuran a comprar boletos de cine al mismo tiempo, los asientos restantes pueden ser mayores que 0, lo que da como resultado que se vendan demasiados boletos, lo que no cumple con las expectativas .

código de muestra :

public class MovieTicket {
    
    
    private int availableTickets;

    public MovieTicket(int totalTickets) {
    
    
        this.availableTickets = totalTickets;
    }

    public void sellTickets(int numTickets, String user) {
    
    
        if (numTickets > availableTickets) {
    
    
            System.out.println("抱歉," + user + ",剩余票数不足!");
            return;
        }

        // 模拟售票过程
        // 例如查库 写库 调用远程服务等
        try {
    
    
            Thread.sleep(100); // 假设售票过程需要一定的时间
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        availableTickets -= numTickets;
        System.out.println(user + "购买了" + numTickets + "张票,剩余票数:" + availableTickets);
    }

    public int getTicketsAvailable() {
    
    
        return availableTickets;
    }
}
  • MovieTicketLa clase representa el sistema de venta de entradas de un cine.
  • sellTicket()El método se usa para vender boletos, cada llamada reducirá la cantidad de boletos restantes y luego generará la información del boleto.
  • getTicketsAvailable()El método se utiliza para obtener los votos restantes.

A continuación, simulemos 10 usuarios comprando entradas de cine al mismo tiempo :

public class Test {
    
    
    public static void main(String[] args) {
    
    
        MovieTicket ticketCounter = new MovieTicket(10);
        for (int i = 0; i < 10; i++) {
    
    
            int finalI = i;
            Thread thread1 = new Thread(() -> ticketCounter.sellTickets(1, "User"+ finalI));
            thread1.start();
        }
    }
}

Los resultados de la ejecución son los siguientes:

User7购买了1张票,剩余票数:3
User1购买了1张票,剩余票数:3
User8购买了1张票,剩余票数:3
User2购买了1张票,剩余票数:3
User9购买了1张票,剩余票数:3
User6购买了1张票,剩余票数:3
User4购买了1张票,剩余票数:3
User5购买了1张票,剩余票数:3
User0购买了1张票,剩余票数:3
User3购买了1张票,剩余票数:3

El resultado esperado es el siguiente:

User0购买了1张票,剩余票数:9
User8购买了1张票,剩余票数:8
User7购买了1张票,剩余票数:7
User9购买了1张票,剩余票数:6
User6购买了1张票,剩余票数:5
User3购买了1张票,剩余票数:4
User5购买了1张票,剩余票数:3
User4购买了1张票,剩余票数:2
User2购买了1张票,剩余票数:1
User1购买了1张票,剩余票数:0

lo que sucederá

Cuando varios subprocesos leen y escriben en un recurso compartido al mismo tiempo , pueden surgir condiciones de carrera de datos, lo que hace que los datos se contaminen o produzcan resultados indeterminados.

Un recurso compartido puede ser una variable de contador , una matriz , un registro en una base de datos o cualquier otra cosa .

Las operaciones comunes son:

  • Operación de verificar y luego actuar (inicialización)
  • Operación de lectura-modificación-escritura (contador de incrementos)

Cómo garantizar la seguridad de los hilos

entorno independiente

1. Diseño sin estado

Diseñe clases que no tengan estado, es decir, clases que no tengan variables globales ni estado compartido. Al evitar la competencia por los recursos compartidos, se reducen los conflictos y las condiciones de carrera entre subprocesos, lo que garantiza la seguridad de los subprocesos. Aquí hay un ejemplo:

código de muestra :

public class ThreadSafeCalculator {
    
    
    // 没有任何全局变量或共享状态
    public int add(int a, int b) {
    
    
        return a + b;
    }

    public int subtract(int a, int b) {
    
    
        return a - b;
    }
    // 其他无状态的计算方法...
}

Al diseñar la clase para que no tenga estado, cada subproceso puede crear su propia instancia o compartir la misma instancia y llamar a métodos de forma independiente para realizar cálculos. Dado que no hay un estado compartido, no hay carrera ni conflicto entre subprocesos, lo que garantiza la seguridad de los subprocesos.

Cabe señalar que el diseño sin estado no es adecuado para todos los escenarios. En algunos casos, es posible que se requieran variables globales o de estado compartido para implementar una funcionalidad específica. En este caso, se deben adoptar mecanismos de sincronización apropiados (como el uso de bloqueos) para garantizar la seguridad del acceso multiproceso a los recursos compartidos.

2. Usa la palabra clave final (inmutable)

La variable final también es segura para subprocesos en Java porque una vez que se asigna una referencia de un objeto, no puede apuntar a una referencia de otro objeto.

Ejemplo de código :

public class ThreadSafeCounter {
    
    
    private final int limit = 100;
    private final Object lock = new Object();

    public void increment() {
    
    
    }

    public int getLimit() {
    
    
        return limit;
    }
}

Cabe señalar que finalla palabra clave solo garantiza que la referencia de la variable no se modificará, pero no garantiza la inmutabilidad del estado interno del objeto referenciado. Si el objeto de referencia en sí mismo es mutable y varios subprocesos lo modifican, aún se requieren mecanismos de sincronización adicionales para garantizar la seguridad de los subprocesos.

3. Usa la palabra clave sincronizada

Utilice la palabra clave sincronizada para modificar el método de acceso o el bloque de código del recurso compartido para asegurarse de que solo un subproceso pueda acceder al recurso compartido al mismo tiempo.

La palabra clave sincronizada puede evitar que varios subprocesos modifiquen los recursos compartidos al mismo tiempo, lo que garantiza la coherencia y corrección de los datos. Cuando un subproceso adquiere el bloqueo, otros subprocesos se bloquearán hasta que se libere el bloqueo.

código de muestra :

public synchronized void sellTickets(int numTickets, String user) {
    
    
    // 线程安全的代码块
    // ...
}

4. Usa la palabra clave volátil

La palabra clave volatile se usa para modificar la variable compartida para garantizar que se lea el último valor de la memoria principal cada vez que se accede a la variable, en lugar de usar el caché local del subproceso. Puede garantizar la visibilidad entre varios subprocesos, pero no puede resolver los problemas de atomicidad y ordenación. Por lo tanto, volatile es adecuado para algunas banderas o interruptores de estado variable simple .

código de muestra :

private volatile boolean flag = false;

public void setFlag(boolean value) {
    
    
    flag = value;
}

public boolean getFlag() {
    
    
    return flag;
}

5. Use la clase contenedora atómica en el paquete java.util.concurrent.atomic

Java proporciona una serie de clases atómicas, como AtomicInteger, AtomicLong, etc., que proporcionan operaciones atómicas que pueden completar operaciones de lectura y actualización en una sola operación, lo que garantiza la seguridad de subprocesos. La clase Atomic utiliza la operación CAS (Comparar e intercambiar) subyacente para garantizar la atomicidad y la visibilidad.

código de muestra :

private AtomicInteger availableTickets = new AtomicInteger(10);

public void sellTickets(int numTickets, String user) {
    
    
    int remainingTickets = availableTickets.getAndAdd(-numTickets);
    // 线程安全的代码块
    // ...
}

6. Use los bloqueos en el paquete java.util.concurrent.locks

ReentrantLock es un bloqueo de reentrada proporcionado por Java, que proporciona más flexibilidad y escalabilidad. Al adquirir y liberar bloqueos explícitamente, puede asegurarse de que solo un subproceso pueda acceder a un recurso compartido. En comparación con la palabra clave sincronizada, ReentrantLock proporciona funciones más avanzadas, como bloqueos interrumpibles, bloqueos justos, etc.

código de muestra :

private ReentrantLock lock = new ReentrantLock();

public void sellTickets(int numTickets, String user) {
    
    
    lock.lock();
    try {
    
    
        // 线程安全的代码块
        // ...
    } finally {
    
    
        lock.unlock();
    }
}

7. Use clases de colección seguras para subprocesos

Java proporciona muchas estructuras de datos seguras para subprocesos, como ConcurrentHashMap, CopyOnWriteArrayList, etc. Estas estructuras de datos implementan internamente mecanismos de modificación y acceso seguros para subprocesos, y se pueden usar directamente en un entorno de subprocesos múltiples sin medidas de sincronización adicionales.

código de muestra :

private Map<String, Integer> map = new ConcurrentHashMap<>();

public void updateMap(String key, int value) {
    
    
    map.put(key, value);
}

8. Usa ThreadLocal

ThreadLocal es un mecanismo de cierre de subprocesos proporcionado por Java, que puede proporcionar a cada subproceso una copia de variable independiente (espacio para el tiempo) . Al almacenar variables compartidas en ThreadLocal, se puede evitar el intercambio de datos y la competencia entre varios subprocesos, lo que garantiza la seguridad de los subprocesos.

código de muestra :

private ThreadLocal<Integer> threadLocalCount = ThreadLocal.withInitial(() -> 0);

public void incrementCount() {
    
    
    int count = threadLocalCount.get();
    threadLocalCount.set(count + 1);
}

entorno de clúster

En un entorno de clúster, se deben considerar más factores y desafíos para garantizar la seguridad de los subprocesos. Dado que los clústeres involucran múltiples servidores y múltiples procesos/subprocesos que se ejecutan simultáneamente, el mantenimiento de la seguridad de los subprocesos se vuelve más complicado. Los siguientes son algunos escenarios comunes para garantizar la seguridad de subprocesos en un entorno de clúster

1. Bloqueo distribuido

  • Utilice bloqueos distribuidos para coordinar el acceso a los recursos compartidos entre varios nodos.
  • Las implementaciones comunes de bloqueos distribuidos incluyen bloqueos basados ​​en bases de datos, bloqueos basados ​​en caché (como bloqueos de Redis) y bloqueos basados ​​en ZooKeeper.
  • Antes de acceder a los recursos compartidos, los nodos deben adquirir bloqueos distribuidos para garantizar que solo un nodo pueda ejecutar código de sección crítica.

Ejemplo de pseudocódigo :

// 加锁
if (acquireLock(key)) {
    
    
    try {
    
    
        // 执行操作
    } finally {
    
    
        // 释放锁
        releaseLock(key);
    }
}

referencia

2. Fragmentación, división y aislamiento de datos

  • Divida los datos compartidos en partes y asigne cada parte a un nodo diferente para su procesamiento.
  • Cada nodo solo es responsable de los fragmentos de datos asignados por sí mismo, lo que evita que varios nodos accedan a los mismos datos al mismo tiempo.
  • Las estrategias de fragmentación de datos adecuadas se pueden seleccionar de acuerdo con las características de los datos y las condiciones de carga, como hash basado en hash, hash consistente, rango, etc.

Ejemplo de pseudocódigo :

// 获取数据分片的节点
Node node = getShardNode(key);
// 在指定节点上执行操作
result = node.processData(key, data);

3. La serialización evita la concurrencia

  • Utilice la cola de mensajes como el middleware para el intercambio de datos y convierta la operación de los recursos compartidos en forma de mensajes asíncronos.
  • Cada nodo recibe mensajes de la cola de mensajes y los procesa, lo que garantiza que solo un nodo procese cada mensaje.
  • Las colas de mensajes pueden proporcionar un mecanismo confiable de entrega de mensajes y garantizar la coherencia de los datos a través del orden de consumo de mensajes.
  • Evitar la concurrencia a través de alguna estrategia y diseño de negocio.
// 发送消息到消息队列
queue.send(key,message);

// 在节点上异步消费消息
queue.consume(key,message -> {
    
    
    // 处理消息
});

4. Operaciones atómicas distribuidas

Redis proporciona algunos comandos atómicos que pueden implementar algunas operaciones atómicas distribuidas comunes en un entorno de clúster. Estos son algunos ejemplos y comandos atómicos de Redis de uso común:

1.SETNX (Establecer si no existe)

Si la clave especificada no existe, establece el valor de la clave en el valor dado, la operación es atómica.

// 设置键名为 "key" 的值为 "value",仅当该键不存在时
jedis.setnx("key", "value");

2. Contadores atómicos

Los comandos INCR y DECR de Redis pueden realizar operaciones atómicas en valores enteros almacenados en Redis.

// 自增计数器
Long incrementedValue = jedis.incr("counter_key");

// 自减计数器
Long decrementedValue = jedis.decr("counter_key");

3. Combinación de operación atómica de transacción

Redis proporciona una combinación de comandos MULTI/EXEC/WATCH, que pueden realizar la ejecución atómica de múltiples operaciones.

// 监视键
jedis.watch("key");

// 开启事务
Transaction transaction = jedis.multi();

// 执行多个操作
transaction.set("key1", "value1");
transaction.set("key2", "value2");

// 提交事务
List<Object> results = transaction.exec();

4. guión lua

Redis proporciona compatibilidad con secuencias de comandos Lua, que se pueden usar para implementar operaciones atómicas más complejas. Al combinar varios comandos de Redis en una secuencia de comandos de Lua, al ejecutar la secuencia de comandos, Redis ejecutará la secuencia de comandos completa como una operación atómica, lo que garantiza que no será interrumpida por otros comandos durante la ejecución.

Puede usar secuencias de comandos de Redis Lua para garantizar la seguridad de subprocesos y evitar problemas de sobreventa.

-- Lua 脚本代码
local key = KEYS[1]  -- 键名
local quantity = ARGV[1]  -- 购买数量

local remaining = tonumber(redis.call('GET', key))  -- 获取当前剩余票数

if remaining and remaining >= tonumber(quantity) then
    redis.call('DECRBY', key, quantity)  -- 减少票数
    return 1  -- 返回成功标志
else
    return 0  -- 返回失败标志
end

En este script de Lua, primero obtenemos los votos restantes actuales para la clave especificada y luego hacemos un juicio basado en la cantidad comprada. Si los votos restantes son suficientes, use el DECRBYcomando Redis para reducir atómicamente la cantidad de votos y devolver una bandera de éxito. De lo contrario, devuelva el indicador de error directamente.

En Java, podemos usar clientes de Redis como Jedis o Lettuce para ejecutar scripts de Lua. El siguiente es un código de muestra para ejecutar scripts de Lua usando Jedis:

Jedis jedis = new Jedis("localhost", 6379);
String script = "local key = KEYS[1]\n" +
                "local quantity = ARGV[1]\n" +
                "local remaining = tonumber(redis.call('GET', key))\n" +
                "if remaining and remaining >= tonumber(quantity) then\n" +
                "    redis.call('DECRBY', key, quantity)\n" +
                "    return 1\n" +
                "else\n" +
                "    return 0\n" +
                "end";
String key = "ticket";
String quantity = "2";

// 执行 Lua 脚本
Long result = (Long) jedis.eval(script, Collections.singletonList(key),Collections.singletonList(quantity));

if (result == 1) {
    
    
    // 购票成功
    System.out.println("购票成功");
} else {
    
    
    // 购票失败
    System.out.println("购票失败");
}

Al ejecutar este script de Lua, podemos garantizar la seguridad de subprocesos en un entorno distribuido y evitar el problema de la sobreventa de entradas de cine. Cuando múltiples subprocesos o nodos ejecutan el script al mismo tiempo, Redis garantizará la atomicidad del script de Lua, asegurando así la corrección y consistencia de la operación de compra de boletos.

Hay muchos tipos de operaciones atómicas en la base de datos, los siguientes son algunos ejemplos comunes en desarrollo :

Contador atómico : realice operaciones atómicas en los contadores de la base de datos, normalmente aumentando o disminuyendo el valor del contador.

Ejemplo: Incrementar un contador de vistas en una tabla de artículos.

UPDATE articles SET view_count = view_count + 1 WHERE id = 456;

5. Operación atómica CAS + Retry/Failfast (solución general - protección de límite de token)

Para garantizar la seguridad de subprocesos en un entorno de clúster, combinar operaciones atómicas y protección de tokens es una solución eficaz. El esquema garantiza la coordinación y la exclusión mutua entre varios subprocesos o nodos mediante el uso de operaciones atómicas y un mecanismo de token. Aquí hay una explicación detallada y un pseudocódigo de muestra para el esquema:

  1. Operaciones atómicas: use operaciones atómicas proporcionadas por bases de datos o sistemas de almacenamiento distribuido para garantizar la coherencia de los datos. Estas operaciones atómicas incluyen adición atómica, actualización atómica, eliminación atómica, etc., y puede elegir la operación atómica adecuada de acuerdo con los requisitos comerciales específicos.
  2. Mecanismo de protección de tokens: antes de realizar un conjunto de operaciones no seguras, introduzca una operación de obtención de tokens. La cantidad de tokens está vinculada a un recurso o capacidad operativa en el clúster. Antes de que cada subproceso o nodo pueda realizar una operación no segura, debe obtener un token del conjunto de tokens. El proceso de obtención de tokens debe ser seguro para subprocesos, lo que se puede lograr mediante operaciones atómicas.
  3. Reintentar/Failfast: si el subproceso o nodo no puede obtener el token, es decir, no puede ingresar a la etapa de operación clave, puede optar por reintentar o abandonar la operación. Un mecanismo de reintento permite que un subproceso espere e intente adquirir un token nuevamente hasta que lo logre. El mecanismo Failfast abandona la operación de inmediato para evitar el desperdicio de recursos.

El siguiente es un pseudocódigo de ejemplo que demuestra un esquema seguro para subprocesos de clúster para operaciones atómicas y protección de tokens:

int maxRetries = 3;
int retryInterval = 100; // milliseconds
int currentRetry = 0;
boolean success = false;

while (!success && currentRetry < maxRetries) {
    
    
        // 尝试获取令牌
        if (threadSafeAcquireToken()) {
    
    
            try {
    
    
                // 执行一组不安全的操作
                executeUnsafeOperations();
                success = true;
            } finally {
    
    
                // 释放令牌
                releaseToken();
            }
        } else {
    
    
            // 没有获取到令牌,选择重试或者放弃操作
            currentRetry++;
            handleRetryOrFail();
               // 拿不到令牌,等待一段时间后重试
            Thread.sleep(retryInterval);
        }
}

Tome el ejemplo de venta de boletos de cine anterior como ejemplo. Este token puede ser un token general o un token comercial. Por ejemplo, el límite de tokens aquí es en realidad un límite de 10 personas por película, es decir, 10 tokens.

Cree un script de Lua acquire_token.luapara obtener el token:

local key = KEYS[1]  -- 令牌池键名
local tokenCount = tonumber(ARGV[1])  -- 需要获取的令牌数量

local currentCount = tonumber(redis.call('GET', key))  -- 获取当前令牌数量

if currentCount and currentCount >= tokenCount then
    redis.call('DECRBY', key, tokenCount)  -- 减少令牌数量
    return 1  -- 获取令牌成功
else
    return 0  -- 获取令牌失败
end

Cree un script Lua release_token.luapara liberar tokens

local key = KEYS[1]  -- 令牌池键名
local tokenCount = tonumber(ARGV[1])  -- 需要释放的令牌数量

redis.call('INCRBY', key, tokenCount)  -- 增加令牌数量

Ejecute scripts de Lua usando Jedis en Java:

int maxRetries = 3;  // 最大重试次数
int retryDelayMillis = 100;  // 重试延迟时间

int retryCount = 0;
boolean acquiredToken = false;

// 获取令牌
while (!acquiredToken && retryCount < maxRetries) {
    
    
    Long acquireResult = (Long) jedis.eval(acquireScript, Collections.singletonList(电影id), Collections.singletonList(String.valueOf(tokenCount)));

    if (acquireResult == 1) {
    
    
        acquiredToken = true;
    } else {
    
    
        retryCount++;
        try {
    
    
            Thread.sleep(retryDelayMillis);
        } catch (InterruptedException e) {
    
    
        }
    }
}
// 处理业务
if (acquiredToken) {
    
    
    try {
    
    
        // 执行线程安全的操作 重点 重点 重点,这里是一大堆操作需要保证线程安全的
        // 远程调用
        // 写库
        // ...
    } finally {
    
     // 释放令牌
        jedis.eval(releaseScript, Collections.singletonList(电影id), Collections.singletonList(String.valueOf(tokenCount)));
    }
} else {
    
    
    // 重试次数超过阈值,执行其他处理逻辑或抛出异常
    // ...
    throw 
}

Resumir

A través de la clasificación y el análisis anteriores, creamos un pseudocódigo basado en la estrategia de protección de restricción de token general basada en Redis .

public class RedisTokenProtection {
    
    
    private final Jedis jedis;
    private final String tokenPoolKey;
    private final int maxRetries;
    private final long retryInterval;

    /**
     * 构造函数
     *
     * @param jedisSupplier 提供 Jedis 实例的供应商
     * @param tokenPoolKey  令牌池的键名
     * @param maxRetries    最大重试次数
     * @param retryInterval 重试间隔时间(毫秒)
     */
    public RedisTokenProtection(Supplier<Jedis> jedisSupplier, String tokenPoolKey, int maxRetries, long retryInterval) {
    
    
        this.jedis = jedisSupplier.get();
        this.tokenPoolKey = tokenPoolKey;
        this.maxRetries = maxRetries;
        this.retryInterval = retryInterval;
    }

    /**
     * 执行带有令牌保护的业务逻辑
     *
     * @param limitTokenCount   限制令牌数
     * @param requestTokenKey   请求令牌的键名
     * @param requestTokenCount 请求令牌的数量
     * @param totalTimeout      总的执行超时时间(毫秒)
     * @param supplier          提供业务逻辑的供应商
     * @param <T>               返回值的类型
     * @return 业务逻辑的返回值
     * @throws TokenAcquisitionException 令牌获取异常
     */
    public <T> T executeWithTokenProtection(int limitTokenCount, String requestTokenKey, int requestTokenCount, long totalTimeout, Supplier<T> supplier) throws TokenAcquisitionException {
    
    
        long startTime = System.currentTimeMillis();
        try {
    
    
            // 尝试获取令牌
            boolean acquiredToken = acquireToken(limitTokenCount, requestTokenKey, requestTokenCount);
            if (acquiredToken) {
    
    
                // 成功获取令牌后执行业务逻辑
                return supplier.get();
            }
            throw new TokenAcquisitionException("Failed to acquire tokens.");
        } catch (TokenAcquisitionException ex) {
    
    
            throw ex;
        } catch (Exception ex) {
    
    
            long elapsedTime = System.currentTimeMillis() - startTime;
            int retries = 0;
            while (retries < maxRetries && elapsedTime < totalTimeout) {
    
    
                try {
    
    
                    // 等待重试间隔
                    Thread.sleep(retryInterval);
                    boolean acquiredToken = acquireToken(limitTokenCount, requestTokenKey, requestTokenCount);
                    if (acquiredToken) {
    
    
                        // 成功获取令牌后执行业务逻辑
                        return supplier.get();
                    }
                    retries++;
                    elapsedTime = System.currentTimeMillis() - startTime;
                } catch (InterruptedException e) {
    
    
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            throw new TokenAcquisitionException("Failed to acquire tokens after retrying " + maxRetries + " times.");
        } finally {
    
    
            releaseToken(requestTokenKey, requestTokenCount);
        }
    }

    // 获取令牌的逻辑,实现方法根据具体需求自行编写
    // 必须是原子的
    private boolean acquireToken(int limitTokenCount, String requestTokenKey, int requestTokenCount) {
    
    
         	String acquireTokenScript = 
                "local availableTokens = tonumber(redis.call('get', KEYS[1])) or 0\n" +
                "if availableTokens >= tonumber(ARGV[1]) then\n" +
                "    redis.call('decrby', KEYS[1], ARGV[1])\n" +
                "    return true\n" +
                "else\n" +
                "    return false\n" +
                "end";

            Object result = jedis.eval(acquireTokenScript, Collections.singletonList(requestTokenKey),
                Collections.singletonList(String.valueOf(requestTokenCount)));

            return (Boolean) result;
    }

    // 释放令牌的逻辑,实现方法根据具体需求自行编写
    // 必须是原子的
    private void releaseToken(String requestTokenKey, int requestTokenCount) {
    
    
     	String releaseTokenScript =
            "redis.call('incrby', KEYS[1], ARGV[1])";

        jedis.eval(releaseTokenScript, Collections.singletonList(requestTokenKey),
            Collections.singletonList(String.valueOf(requestTokenCount)));
    }

    public class TokenAcquisitionException extends Exception {
    
    
        public TokenAcquisitionException(String message) {
    
    
            super(message);
        }
    }
}

Ejemplo de llamada :

public class Main {
    
    
    public static void main(String[] args) {
    
    
        // 创建 Jedis 实例的供应商
        Supplier<Jedis> jedisSupplier = () -> {
    
    
            // 这里创建和配置 Jedis 实例,例如连接到 Redis 服务器
            return new Jedis("localhost");
        };

        // 创建 RedisTokenProtection 实例
        RedisTokenProtection tokenProtection = new RedisTokenProtection(jedisSupplier, "token_pool:", 3, 1000);

        try {
    
    
            // 执行带有令牌保护的业务逻辑
            String movieId = "亮剑";
            boolean result = tokenProtection.executeWithTokenProtection(10, movieId, 1, 10000, () -> {
    
    
                // 这里编写需要保护的线程不安全的业务逻辑
                System.out.println("执行业务逻辑...");
                // 假设这里有一段需要保护的代码
                // ...

                // 返回业务逻辑执行的结果
                return true;
            });
            if (result) {
    
    
                System.out.println("业务逻辑执行成功!");
            } else {
    
    
                System.out.println("业务逻辑执行失败!");
            }
        } catch (RedisTokenProtection.TokenAcquisitionException ex) {
    
    
            System.out.println("获取令牌失败:" + ex.getMessage());
        }
    }
}

Cuando se usa la combinación de Spring AOP y anotaciones personalizadas, la función de protección de token se puede realizar de manera más conveniente. Aquí hay un código de muestra que muestra cómo implementar la protección de token usando Spring AOP y anotaciones personalizadas:

Primero, defina una anotación personalizada TokenProtectedpara marcar el método que necesita protección de token:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenProtected {
    
    
    int limitTokenCount() default 1;
    String requestTokenKey();
    int requestTokenCount() default 1;
    long totalTimeout() default 0;
}

Luego, cree una clase de aspecto TokenProtectionAspectpara implementar la lógica de protección de token usando Spring AOP

@Aspect
@Component
public class TokenProtectionAspect {
    
    
    private final RedisTokenProtection tokenProtection;

    @Autowired
    public TokenProtectionAspect(RedisTokenProtection tokenProtection) {
    
    
        this.tokenProtection = tokenProtection;
    }

    @Pointcut("@annotation(com.example.TokenProtected)")
    public void tokenProtectedMethod() {
    
    
    }

    @Around("tokenProtectedMethod() && @annotation(tokenProtected)")
    public Object protectWithToken(ProceedingJoinPoint joinPoint, TokenProtected tokenProtected) throws Throwable {
    
    
        int limitTokenCount = tokenProtected.limitTokenCount();
        String requestTokenKey = tokenProtected.requestTokenKey();
        int requestTokenCount = tokenProtected.requestTokenCount();
        long totalTimeout = tokenProtected.totalTimeout();

        Supplier<Object> supplier = () -> {
    
    
            try {
    
    
                return joinPoint.proceed();
            } catch (Throwable throwable) {
    
    
                throw new RuntimeException(throwable);
            }
        };

        return tokenProtection.executeWithTokenProtection(limitTokenCount, requestTokenKey, requestTokenCount, totalTimeout, supplier);
    
    }
}

En esta clase de aspecto, definimos un punto de corte tokenProtectedMethod()para que coincida con el TokenProtectedmétodo anotado. En protectWithTokenel método, obtenemos TokenProtectedlos parámetros de la anotación y creamos una RedisTokenProtectioninstancia para ejecutar la lógica de protección del token.

Finalmente, al usarlo, solo necesita agregar anotaciones a los métodos que necesitan protección de token @TokenProtectedy configurar los parámetros correspondientes:

@Service
public class MyService {
    
    
    @TokenProtected(limitTokenCount = 100, requestTokenKey = "myTokenKey", requestTokenCount = 1, totalTimeout = 5000)
    public void protectedMethod() {
    
    
        // 令牌保护的业务逻辑
    }
}

En el ejemplo anterior, protectedMethodel método está marcado como que requiere protección de token y se proporcionan los parámetros de token relevantes.

A través de los pasos anteriores, puede usar Spring AOP y anotaciones personalizadas para implementar un mecanismo de protección de token conveniente y fácil de usar. La clase de aspecto intercepta @TokenProtectedel método anotado y adquiere y libera tokens antes y después de la ejecución.

Supongo que te gusta

Origin blog.csdn.net/abu935009066/article/details/131366487
Recomendado
Clasificación