Comment assurer la sécurité des threads de code dans des environnements autonomes et en cluster


Dans la programmation multithread, la sécurité des threads est un concept clé, qui implique l'exactitude et la cohérence lorsque plusieurs threads accèdent et modifient des ressources partagées. Que ce soit dans un environnement autonome ou dans un environnement de cluster distribué, la sécurité des threads est un problème auquel les développeurs doivent prêter attention.

image

Qu'est-ce que la sécurité des fils

La sécurité des threads est un terme de conception de programmation, ce qui signifie que lorsqu'une fonction ou une bibliothèque de fonctions est appelée dans un environnement multithread , elle peut gérer correctement les variables communes entre plusieurs threads , afin que la fonction du programme puisse être exécutée correctement .

Ce qui précède provient de Wikipédia

Donnez un exemple pour illustrer l'importance de la sécurité des threads. Supposons qu'il y ait une salle de cinéma diffusant un film avec un total de 10 sièges. S'il n'y a pas de mesures de protection thread-safe, lorsque plusieurs personnes se précipitent pour acheter des billets de cinéma en même temps, les sièges restants peuvent être supérieurs à 0, ce qui entraîne la vente d'un trop grand nombre de billets, ce qui ne répond pas aux attentes .

Exemple de code :

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 classe représente le système de billetterie d'une salle de cinéma
  • sellTicket()La méthode est utilisée pour vendre des billets, chaque appel réduira le nombre de billets restants, puis sortira les informations du billet.
  • getTicketsAvailable()La méthode est utilisée pour obtenir les votes restants

Ensuite, simulons 10 utilisateurs achetant des billets de cinéma en même temps :

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();
        }
    }
}

Les résultats d'exécution sont les suivants :

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

Le résultat attendu est le suivant :

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

que va-t-il se passer

Lorsque plusieurs threads lisent et écrivent simultanément sur une ressource partagée , des conditions de concurrence des données peuvent survenir, provoquant la pollution des données ou produisant des résultats indéterminés.

Une ressource partagée peut être une variable compteur , un tableau , un enregistrement dans une base de données ou n'importe quoi d' autre .

Les opérations courantes sont :

  • Opération vérifier puis agir (initialisation)
  • Opération de lecture-modification-écriture (incrémentation du compteur)

Comment assurer la sécurité des threads

environnement autonome

1. Conception sans état

Concevez des classes sans état, c'est-à-dire des classes qui n'ont pas de variables globales ni d'état partagé. En évitant la concurrence pour les ressources partagées, les conflits et les conditions de concurrence entre les threads sont réduits, garantissant ainsi la sécurité des threads. Voici un exemple:

Exemple de code :

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

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

En concevant la classe sans état, chaque thread peut créer sa propre instance ou partager la même instance et appeler des méthodes indépendamment pour effectuer des calculs. Puisqu'il n'y a pas d'état partagé, il n'y a pas de course ou de conflit entre les threads, garantissant ainsi la sécurité des threads.

Il convient de noter que la conception sans état ne convient pas à tous les scénarios. Dans certains cas, des variables d'état ou globales partagées peuvent en effet être nécessaires pour implémenter des fonctionnalités spécifiques. Dans ce cas, des mécanismes de synchronisation appropriés (tels que l'utilisation de verrous) doivent être adoptés pour garantir la sécurité de l'accès multithread aux ressources partagées.

2. Utilisez le dernier mot-clé (immuable)

La variable finale est également thread-safe en Java car une fois qu'une référence d'un objet est assignée, elle ne peut pas pointer vers une référence d'un autre objet.

Exemple de code :

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

    public void increment() {
    
    
    }

    public int getLimit() {
    
    
        return limit;
    }
}

A noter que finalle mot-clé garantit uniquement que la variable référence ne sera pas modifiée, mais ne garantit pas l'immuabilité de l'état interne de l'objet référencé. Si l'objet de référence lui-même est modifiable et que plusieurs threads le modifient, des mécanismes de synchronisation supplémentaires sont toujours nécessaires pour garantir la sécurité des threads.

3. Utilisez le mot-clé synchronisé

Utilisez le mot clé synchronized pour modifier la méthode d'accès ou le bloc de code de la ressource partagée afin de vous assurer qu'un seul thread peut accéder à la ressource partagée à la fois.

Le mot clé synchronized peut empêcher plusieurs threads de modifier les ressources partagées en même temps, garantissant ainsi la cohérence et l'exactitude des données. Lorsqu'un thread acquiert le verrou, les autres threads seront bloqués jusqu'à ce que le verrou soit libéré.

Exemple de code :

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

4. Utilisez le mot clé volatile

Le mot-clé volatile est utilisé pour modifier la variable partagée afin de s'assurer que la dernière valeur est lue à partir de la mémoire principale à chaque accès à la variable, au lieu d'utiliser le cache local du thread. Il peut assurer la visibilité entre plusieurs threads, mais il ne peut pas résoudre les problèmes d'atomicité et d'ordre. Par conséquent, volatile convient à certains indicateurs ou commutateurs d'état variables simples .

Exemple de code :

private volatile boolean flag = false;

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

public boolean getFlag() {
    
    
    return flag;
}

5. Utilisez la classe wrapper atomique dans le package java.util.concurrent.atomic

Java fournit une série de classes atomiques, telles que AtomicInteger, AtomicLong, etc., qui fournissent des opérations atomiques, qui peuvent effectuer des opérations de lecture et de mise à jour en une seule opération, garantissant ainsi la sécurité des threads. La classe Atomic utilise l'opération CAS (Compare and Swap) sous-jacente pour garantir l'atomicité et la visibilité.

Exemple de code :

private AtomicInteger availableTickets = new AtomicInteger(10);

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

6. Utilisez les verrous du package java.util.concurrent.locks

ReentrantLock est un verrou réentrant fourni par Java, qui offre plus de flexibilité et d'évolutivité. En acquérant et en libérant explicitement des verrous, vous pouvez vous assurer qu'un seul thread peut accéder à une ressource partagée. Par rapport au mot-clé synchronisé, ReentrantLock fournit des fonctionnalités plus avancées, telles que les verrous interruptibles, les verrous équitables, etc.

Exemple de code :

private ReentrantLock lock = new ReentrantLock();

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

7. Utilisez des classes de collection thread-safe

Java fournit de nombreuses structures de données thread-safe, telles que ConcurrentHashMap, CopyOnWriteArrayList, etc. Ces structures de données implémentent en interne des mécanismes d'accès et de modification thread-safe et peuvent être utilisées directement dans un environnement multi-thread sans mesures de synchronisation supplémentaires.

Exemple de code :

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

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

8. Utilisez ThreadLocal

ThreadLocal est un mécanisme de fermeture de thread fourni par Java, qui peut fournir à chaque thread une copie de variable indépendante (espace pour le temps) . En stockant des variables partagées dans ThreadLocal, le partage de données et la concurrence entre plusieurs threads peuvent être évités, garantissant ainsi la sécurité des threads.

Exemple de code :

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

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

environnement de grappe

Dans un environnement de cluster, davantage de facteurs et de défis doivent être pris en compte pour garantir la sécurité des threads. Comme les clusters impliquent plusieurs serveurs et plusieurs processus/threads exécutés simultanément, la maintenance de la sécurité des threads devient plus compliquée. Voici quelques scénarios courants pour garantir la sécurité des threads dans un environnement de cluster

1. Serrure distribuée

  • Utilisez des verrous distribués pour coordonner l'accès aux ressources partagées entre plusieurs nœuds.
  • Les implémentations courantes de verrous distribués incluent les verrous basés sur la base de données, les verrous basés sur le cache (tels que les verrous Redis) et les verrous basés sur ZooKeeper.
  • Avant d'accéder aux ressources partagées, les nœuds doivent acquérir des verrous distribués pour s'assurer qu'un seul nœud peut exécuter le code de section critique.

Exemple de pseudo-code :

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

référence

2. Partage des données, fractionnement et isolation des données

  • Divisez les données partagées en morceaux et affectez chaque morceau à un nœud différent pour le traitement.
  • Chaque nœud n'est responsable que des fragments de données alloués par lui-même, empêchant plusieurs nœuds d'accéder aux mêmes données en même temps.
  • Des stratégies de fragmentation de données appropriées peuvent être sélectionnées en fonction des caractéristiques des données et des conditions de charge, telles que le hachage, le hachage cohérent, la plage, etc.

Exemple de pseudo-code :

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

3. La sérialisation évite la concurrence

  • Utilisez la file d'attente de messages comme middleware pour l'échange de données et convertissez le fonctionnement des ressources partagées sous la forme de messages asynchrones.
  • Chaque nœud reçoit des messages de la file d'attente de messages et les traite, garantissant qu'un seul nœud traite chaque message.
  • Les files d'attente de messages peuvent fournir un mécanisme de livraison de messages fiable et assurer la cohérence des données grâce à l'ordre de consommation des messages.
  • Évitez la concurrence grâce à une stratégie et à une conception commerciale.
// 发送消息到消息队列
queue.send(key,message);

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

4. Opérations atomiques distribuées

Redis fournit des commandes atomiques qui peuvent implémenter certaines opérations atomiques distribuées courantes dans un environnement de cluster. Voici quelques commandes et exemples atomiques Redis couramment utilisés :

1.SETNX (Définir s'il n'existe pas)

Si la clé spécifiée n'existe pas, définit la valeur de la clé sur la valeur donnée, l'opération est atomique.

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

2. Compteurs atomiques

Les commandes INCR et DECR de Redis peuvent effectuer des opérations atomiques sur des valeurs entières stockées dans Redis.

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

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

3. Combinaison d'opérations atomiques de transaction

Redis fournit une combinaison de commandes MULTI/EXEC/WATCH, qui peuvent réaliser l'exécution atomique de plusieurs opérations.

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

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

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

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

4. script lua

Redis fournit la prise en charge des scripts Lua, qui peuvent être utilisés pour implémenter des opérations atomiques plus complexes. En combinant plusieurs commandes Redis dans un script Lua, lors de l'exécution du script, Redis exécutera le script entier comme une opération atomique, garantissant qu'il ne sera pas interrompu par d'autres commandes pendant l'exécution.

Vous pouvez utiliser des scripts Redis Lua pour garantir la sécurité des threads et éviter les problèmes de survente.

-- 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

Dans ce script Lua, nous obtenons d'abord les votes restants actuels pour la clé spécifiée, puis nous rendons un jugement en fonction de la quantité achetée. Si les votes restants sont suffisants, utilisez la DECRBYcommande Redis pour réduire de manière atomique le nombre de votes et renvoyer un indicateur de réussite. Sinon, renvoyez directement l'indicateur d'échec.

En Java, nous pouvons utiliser des clients Redis tels que Jedis ou Lettuce pour exécuter des scripts Lua. Voici un exemple de code pour exécuter des scripts Lua à l'aide de 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("购票失败");
}

En exécutant ce script Lua, nous pouvons assurer la sécurité des threads dans un environnement distribué et éviter le problème de la survente des billets de cinéma. Lorsque plusieurs threads ou nœuds exécutent le script en même temps, Redis garantit l'atomicité du script Lua, assurant ainsi l'exactitude et la cohérence de l'opération d'achat de billets.

Il existe de nombreux types d'opérations atomiques dans la base de données, voici quelques exemples courants en développement :

Compteur atomique : effectuez des opérations atomiques sur les compteurs de la base de données, généralement en augmentant ou en diminuant la valeur du compteur.

Exemple : Incrémentation d'un compteur de vues dans une table d'articles.

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

5. Opération atomique CAS + Retry/Failfast (solution générale - protection de limite de jeton)

Pour garantir la sécurité des threads dans un environnement de cluster, combiner les opérations atomiques et la protection des jetons est une solution efficace. Le schéma assure la coordination et l'exclusion mutuelle entre plusieurs threads ou nœuds en utilisant des opérations atomiques et un mécanisme de jeton. Voici une explication détaillée et un exemple de pseudocode pour le schéma :

  1. Opérations atomiques : utilisez des opérations atomiques fournies par des bases de données ou des systèmes de stockage distribués pour assurer la cohérence des données. Ces opérations atomiques incluent l'ajout atomique, la mise à jour atomique, la suppression atomique, etc., et vous pouvez choisir l'opération atomique appropriée en fonction des besoins spécifiques de l'entreprise.
  2. Mécanisme de protection de jeton : avant d'effectuer un ensemble d'opérations non sécurisées, introduisez une opération de récupération de jeton. Le nombre de jetons est lié à une ressource ou à une capacité opérationnelle dans le cluster. Avant que chaque thread ou nœud puisse effectuer une opération non sécurisée, il doit obtenir un jeton du pool de jetons. Le processus d'obtention de jetons doit être thread-safe, ce qui peut être réalisé à l'aide d'opérations atomiques.
  3. Retry/Failfast : si le thread ou le nœud ne peut pas obtenir le jeton, c'est-à-dire qu'il ne peut pas entrer dans l'étape d'opération clé, vous pouvez choisir de réessayer ou d'abandonner l'opération. Un mécanisme de nouvelle tentative permet à un thread d'attendre et d'essayer d'acquérir à nouveau un jeton jusqu'à ce qu'il réussisse. Le mécanisme Failfast abandonne immédiatement l'opération pour éviter de gaspiller des ressources.

Voici un exemple de pseudocode qui illustre un schéma de cluster thread-safe pour les opérations atomiques et la protection des jetons :

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);
        }
}

Prenons l'exemple de la vente de billets de cinéma ci-dessus. Ce jeton peut être un jeton général ou un jeton d'entreprise. Par exemple, la limite de jetons ici est en fait une limite de 10 personnes par film, soit 10 jetons.

Créez un script Lua acquire_token.luapour obtenir le jeton :

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

Créer un script Lua release_token.luapour libérer des jetons

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

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

Exécutez des scripts Lua en utilisant 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 
}

Résumer

Grâce au tri et à l'analyse ci-dessus, nous créons un pseudocode basé sur la stratégie générale de protection des restrictions de jetons basée sur 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);
        }
    }
}

Exemple d'appel :

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());
        }
    }
}

Lorsque vous utilisez la combinaison de Spring AOP et d'annotations personnalisées, la fonction de protection des jetons peut être réalisée plus facilement. Voici un exemple de code montrant comment implémenter la protection des jetons à l'aide de Spring AOP et d'annotations personnalisées :

Tout d'abord, définissez une annotation personnalisée TokenProtectedpour marquer la méthode qui nécessite une protection par jeton :

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

Ensuite, créez une classe d'aspect TokenProtectionAspectpour implémenter la logique de protection des jetons à l'aide de 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);
    
    }
}

Dans cette classe d'aspect, nous définissons un point de coupure tokenProtectedMethod()pour correspondre à la TokenProtectedméthode annotée. Dans protectWithTokenla méthode, nous obtenons TokenProtectedles paramètres de l'annotation et créons une RedisTokenProtectioninstance pour exécuter la logique de protection des jetons.

Enfin, lors de son utilisation, il vous suffit d'ajouter des annotations aux méthodes nécessitant une protection par jeton @TokenProtectedet de configurer les paramètres correspondants :

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

Dans l'exemple ci-dessus, protectedMethodla méthode est marquée comme nécessitant une protection de jeton et les paramètres de jeton pertinents sont fournis.

Au cours des étapes ci-dessus, vous pouvez utiliser Spring AOP et des annotations personnalisées pour implémenter un mécanisme de protection de jeton pratique et facile à utiliser. La classe d'aspect intercepte @TokenProtectedla méthode annotée et acquiert et libère des jetons avant et après l'exécution.

Je suppose que tu aimes

Origine blog.csdn.net/abu935009066/article/details/131366487
conseillé
Classement