Implémentation Redis, redisson et écriture manuscrite des verrous distribués

Redis du verrou distribué

Redisson

Introduire la réduction:

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

La classe d'outils de verrouillage RLock a été encapsulée dans RedissonClient, et je l'utilise directement ici:

package com.morris.distribute.lock.redis.redisson;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {
    
    

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 分布式锁之redis(redisson实现)
     *
     * @param id
     */
    public void updateStatus(int id) {
    
    
        log.info("updateStatus begin, {}", id);

        String key =  "updateStatus" + id;

        RLock lock = redissonClient.getLock(key);
        lock.lock(); // 加锁
        try {
    
    
            Integer status = jdbcTemplate.queryForObject("select status from t_order where id=?", new Object[]{
    
    id}, Integer.class);

            if (Order.ORDER_STATUS_NOT_PAY == status) {
    
    

                try {
    
    
                    // 模拟耗时操作
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }

                int update = jdbcTemplate.update("update t_order set status=? where id=? and status=?", new Object[]{
    
    2, id, Order.ORDER_STATUS_NOT_PAY});

                if (update > 0) {
    
    
                    log.info("updateStatus success, {}", id);
                } else {
    
    
                    log.info("updateStatus failed, {}", id);
                }
            } else {
    
    
                log.info("updateStatus status already updated, ignore this request, {}", id);
            }
            log.info("updateStatus end, {}", id);
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
    }

}

Les résultats sont les suivants:

2020-09-16 14:43:20,778  INFO [main] (Version.java:41) - Redisson 3.12.4
2020-09-16 14:43:21,298  INFO [redisson-netty-2-16] (ConnectionPool.java:167) - 1 connections initialized for 10.0.4.211/10.0.4.211:6379
2020-09-16 14:43:21,300  INFO [redisson-netty-2-19] (ConnectionPool.java:167) - 24 connections initialized for 10.0.4.211/10.0.4.211:6379
2020-09-16 14:43:21,371  INFO [t2] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:21,371  INFO [t1] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:21,371  INFO [t3] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:24,610  INFO [t3] (OrderService.java:51) - updateStatus success, 1
2020-09-16 14:43:24,610  INFO [t3] (OrderService.java:58) - updateStatus end, 1
2020-09-16 14:43:24,620  INFO [t1] (OrderService.java:56) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:43:24,620  INFO [t1] (OrderService.java:58) - updateStatus end, 1
2020-09-16 14:43:24,630  INFO [t2] (OrderService.java:56) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:43:24,630  INFO [t2] (OrderService.java:58) - updateStatus end, 1

Réalisation de jedis

Implémentons manuellement les verrous distribués redis via jedis et comprenons mieux le principe de l'implémentation des verrous distribués redis.

Présentez les jedis:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

Le raccourci clavier SET key value NX PX miliseconds:.

Qu'est-ce qu'un verrou réussi? Quiconque appelle la commande set avec l'option nx, la clé n'existe pas, le paramètre réussit, sinon il échoue, et celui qui définit la clé avec succès obtiendra le verrou.

Que dois-je faire si le client raccroche? Vous pouvez définir un délai d'expiration pour la clé. Si le client raccroche après son verrouillage, la clé sera supprimée à la fin du temps sans provoquer de blocage.

Que faire si la clé est sur le point d'expirer et que l'entreprise n'a pas été traitée dans le délai d'expiration? Démarrez un thread comme clé pour retarder.

La mise en œuvre spécifique est la suivante:

package com.morris.distribute.lock.redis.my;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RedisLock {
    
    

    @Autowired
    private JedisPool jedisPool;

    public void lock(String key, String value) {
    
    
        for (; ;) {
    
     // 自旋获取锁
            if (tryLock(key, value)) {
    
    
                return;
            }
            try {
    
    
                TimeUnit.MILLISECONDS.sleep(100); // 这里暂时休眠100ms后再次获取锁,后续可以向AQS一样使用等待队列实现
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    /**
     * 尝试加锁
     *
     * @param key
     * @param value
     * @return
     */
    private boolean tryLock(String key, String value) {
    
    
        SetParams setParams = SetParams.setParams().nx().px(4_000); // 默认超时时间为4s
        Jedis jedis = jedisPool.getResource();
        String result = jedis.set(key, value, setParams);
        if ("OK".equals(result)) {
    
    
            Thread thread = new Thread(() -> {
    
    
                while (true) {
    
    
                    try {
    
    
                        TimeUnit.SECONDS.sleep(1); // 守护线程1s检测一下超时时间
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    Long ttl = jedis.pttl(key);
                    if (ttl < 2_000) {
    
     // 当超时时间小于1/2时,增加超时时间到原来的4s
                        jedis.expire(key, 4_000);
                        log.info("add expire time for key : {}", key);
                    }
                }
            }, "expire1");
            thread.setDaemon(true);
            thread.start();
            return true;
        }
        return false;
    }

    public void unlock(String key, String value) {
    
    
        Jedis jedis = jedisPool.getResource();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
    }

}

L'utilisation est la suivante:

package com.morris.distribute.lock.redis.my;

import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderService {
    
    

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private RedisLock redisLock;

    /**
     * 分布式锁之redis(jedis实现)
     *
     * @param id
     */
    public void updateStatus(int id) {
    
    
        log.info("updateStatus begin, {}", id);

        String key =  "updateStatus" + id;
        String value = UUID.randomUUID().toString();

        redisLock.lock(key, value);
        try {
    
    
            Integer status = jdbcTemplate.queryForObject("select status from t_order where id=?", new Object[]{
    
    id}, Integer.class);

            if (Order.ORDER_STATUS_NOT_PAY == status) {
    
    

                try {
    
    
                    // 模拟耗时操作
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }

                int update = jdbcTemplate.update("update t_order set status=? where id=? and status=?", new Object[]{
    
    2, id, Order.ORDER_STATUS_NOT_PAY});

                if (update > 0) {
    
    
                    log.info("updateStatus success, {}", id);
                } else {
    
    
                    log.info("updateStatus failed, {}", id);
                }
            } else {
    
    
                log.info("updateStatus status already updated, ignore this request, {}", id);
            }
            log.info("updateStatus end, {}", id);
        } finally {
    
    
            redisLock.unlock(key, value); // 释放锁
        }
    }
}

Les résultats sont les suivants:

2020-09-16 16:19:01,453  INFO [t2] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:01,453  INFO [t1] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:01,453  INFO [t3] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:03,565  INFO [expire1] (RedisLock.java:53) - add expire time for key : updateStatus1
2020-09-16 16:19:04,748  INFO [t1] (OrderService.java:49) - updateStatus success, 1
2020-09-16 16:19:04,749  INFO [t1] (OrderService.java:56) - updateStatus end, 1
2020-09-16 16:19:04,801  INFO [t2] (OrderService.java:54) - updateStatus status already updated, ignore this request, 1
2020-09-16 16:19:04,801  INFO [t2] (OrderService.java:56) - updateStatus end, 1
2020-09-16 16:19:04,902  INFO [t3] (OrderService.java:54) - updateStatus status already updated, ignore this request, 1
2020-09-16 16:19:04,902  INFO [t3] (OrderService.java:56) - updateStatus end, 1

Pourquoi ne pas deux étapes, d'abord set key valueencore expire key millseconds? Étant donné que ces deux opérations ne sont pas des opérations atomiques, si un client raccroche après avoir été verrouillé, la clé ne sera jamais supprimée, provoquant un blocage.

Pourquoi démarrer un thread démon pour retarder la clé? Le thread démon sera automatiquement détruit lorsque le thread qui l'a créé est fermé, sans arrêt manuel. Le délai consiste à permettre à l'exécution de la logique métier de se terminer, à éviter l'expiration de la clé et à laisser d'autres threads saisir le verrou.

Pourquoi ne pas envoyer la del keycommande directement lors du déverrouillage ? Lorsque vous libérez le verrou, vous devez vérifier la valeur pour éviter que le verrou ajouté par le processus P1 ne soit libéré par d'autres processus. Par conséquent, la définition de la valeur de valeur est également exquise. Seul le processus P1 connaît cette valeur, de sorte que lui seul peut supprimer la clé lorsqu'elle est libérée.

Pour résumer

Avantages: Les verrous distribués basés sur redis auront les caractéristiques de redis, c'est-à-dire rapides.

Inconvénients: la logique d'implémentation est complexe. Redis lui-même est un modèle AP, qui ne peut garantir que la partition et la disponibilité du réseau, mais ne peut pas garantir une forte cohérence. La logique de verrouillage distribué est un modèle CP et la cohérence doit être garantie, redis est donc une méthode d'implémentation Il est probable que plusieurs clients acquerront le verrou. Par exemple, le nœud maître dans redis réussit à définir la clé et à la renvoyer au client. À ce stade, il se bloque avant de pouvoir se synchroniser avec l'esclave, puis l'esclave est élu comme nouveau nœud maître. , D'autres clients réussiront à acquérir le verrou, de sorte que plusieurs clients puissent acquérir le verrou en même temps.

Je suppose que tu aimes

Origine blog.csdn.net/u022812849/article/details/108645002
conseillé
Classement