Распределенная блокировка на основе Redis

интервью Redis задняя часть GitHub

предисловие

Распределенные блокировки широко используются в распределенных приложениях.Если вы хотите понять новую вещь, вы должны сначала понять ее происхождение, чтобы вы могли лучше понять ее и даже сделать из нее выводы.

Прежде всего, когда речь идет о распределенных блокировках, естественно думать о распределенных приложениях.

В автономной системе, прежде чем мы разделим приложение на распределенные приложения, его можно легко использовать для некоторых параллельных сценариев, таких как вычет инвентаря и продажа билетов на автобус при чтении общедоступных ресурсов.Синхронизироватьилизамокможет быть достигнут.

Однако после того, как приложение было распространено, система изменилась с прежней однопроцессорной многопоточной программы на многопроцессорную многопоточную программу На данный момент приведенных выше решений явно недостаточно.

Таким образом, наиболее часто используемым решением в отрасли является использование стороннего компонента и использование его собственной исключительности для достижения взаимного исключения нескольких процессов. как:

  • Уникальный индекс на основе БД.
  • Временные упорядоченные узлы на основе ZK.
  • на основе RedisNX EXпараметр.

Это обсуждение в основном основано на Redis.

выполнить

Поскольку выбран Redis, он должен быть эксклюзивным. При этом лучше всего иметь некоторые основные характеристики замков:

  • Высокая производительность (высокая производительность при добавлении и разблокировке)
  • Можно использовать блокирующие и неблокирующие блокировки.
  • Взаимная блокировка не может произойти.
  • Доступность (отсутствие сбоя блокировки после выхода узла из строя).

Используйте здесьRedis set keyПараметр NX в то время может гарантировать, что запись будет успешной в случае, если этот ключ не существует. А с параметром EX ключ можно автоматически удалить по истечении таймаута.

Таким образом, использование двух вышеупомянутых функций может гарантировать, что только один процесс получит блокировку одновременно, и не будет взаимоблокировки (в худшем случае ключ будет автоматически удален с течением времени).

замок

Код реализации выглядит следующим образом:


    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public  boolean tryLock(String key, String request) {
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);

        if (LOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

Обратите внимание на использование джедаев здесь

String set(String key, String value, String nxxx, String expx, long time);

API

Эта команда гарантирует атомарность NX EX.

Убедитесь, что вы не выполняете две команды (NX EX) по отдельности, если есть проблема с программой после NX, может возникнуть взаимоблокировка.

блокирующий замок

Также можно реализовать блокировку блокировки:

    //一直阻塞
    public void lock(String key, String request) throws InterruptedException {

        for (;;){
            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                break ;
            }
				
			  //防止一直消耗 CPU 	
            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }

    }
    
     //自定义阻塞时间
     public boolean lock(String key, String request,int blockTime) throws InterruptedException {

        while (blockTime >= 0){

            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                return true ;
            }
            blockTime -= DEFAULT_SLEEP_TIME ;

            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }
        return false ;
    }

разблокировать

Разблокировка тоже очень простая, по сути достаточно просто удалить этот ключ и все будет нормально, как например с помощьюdel keyЗаказ.

Но реальность часто не так проста.

Если процесс А захватывает блокировку и устанавливает тайм-аут, но из-за длительного цикла выполнения блокировка автоматически снимается по истечении тайм-аута. В это время процесс B получает блокировку и снимает блокировку вскоре после выполнения. Таким образом, процесс B снимет блокировку процесса A.

Так что лучший способ - оценивать замок каждый раз, когда он разблокирован.Это ты самиз.

На этот раз его необходимо реализовать в сочетании с запорным механизмом.

При блокировке вам нужно передать параметр в качестве значения ключа, чтобы вы могли судить, равно ли значение при каждой разблокировке.

Так что код разблокировки не может быть простымdel.

    public  boolean unlock(String key,String request){
        //lua script
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Object result = null ;
        if (jedis instanceof Jedis){
            result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else if (jedis instanceof JedisCluster){
            result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else {
            //throw new RuntimeException("instance is error") ;
            return false ;
        }

        if (UNLOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

используется здесьluaСкрипт, чтобы определить, равны ли значения, и выполнить команду del, только если они равны.

использоватьluaАтомарность этих двух операций также может быть гарантирована.

Следовательно, четыре основные характеристики, упомянутые выше, также могут быть удовлетворены:

  • Использование Redis гарантирует производительность.
  • См. выше блокирующие и неблокирующие блокировки.
  • Тупик решается с помощью механизма тайм-аута.
  • Redis поддерживает кластерное развертывание для повышения доступности.

использовать

У меня есть полная реализация, и она использовалась для производства, и заинтересованные друзья могут использовать ее из коробки:

maven-зависимости:

<dependency>
    <groupId>top.crossoverjie.opensource</groupId>
    <artifactId>distributed-redis-lock</artifactId>
    <version>1.0.0</version>
</dependency>

Настройте бин:

@Configuration
public class RedisLockConfig {

    @Bean
    public RedisLock build(){
        RedisLock redisLock = new RedisLock() ;
        HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
        JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
        // Jedis 或 JedisCluster 都可以
        redisLock.setJedisCluster(jedisCluster) ;
        return redisLock ;
    }

}

использовать:

    @Autowired
    private RedisLock redisLock ;

    public void use() {
        String key = "key";
        String request = UUID.randomUUID().toString();
        try {
            boolean locktest = redisLock.tryLock(key, request);
            if (!locktest) {
                System.out.println("locked error");
                return;
            }


            //do something

        } finally {
            redisLock.unlock(key,request) ;
        }

    }

Это просто в использовании. Основная цель здесь — использовать Spring, чтобы помочь нам управлять одноэлементным компонентом RedisLock, поэтому при снятии блокировки нам нужно вручную (поскольку во всем контексте есть только один экземпляр RedisLock) входящий ключ и запрос (апи не кажется быть особенно элегантным).

Вы также можете создать новый RedisLock и передавать ключ и запрашивать каждый раз, когда используется блокировка, что очень удобно при разблокировке. Но вам нужно самостоятельно управлять экземпляром RedisLock. У каждого есть преимущества и недостатки.

Исходный код проекта находится по адресу:

GitHub.com/crossover J я…

Обсуждения приветствуются.

одиночный тест

Работая над этим проектом, я должен был подумать об этом.одиночный тест.

Потому что это приложение сильно зависит от сторонних компонентов (Redis), а нам нужно исключить эту зависимость в одиночном тесте. Например, другие партнеры разветвляют проект и хотят запустить одиночный тест локально, но результат не работает:

  1. Возможно, IP, порт и отдельный тест Redis несовместимы.
  2. У самого Redis тоже могут быть проблемы.
  3. Также возможно, что в среде ученика нет Redis.

Так что лучше исключить эти внешние нестабильные факторы и тестировать только написанный нами код.

Таким образом, вы можете ввести единый инструмент тестированияMock.

Его идея очень проста — блокировать все внешние ресурсы, на которые вы полагаетесь. Например: база данных, внешний интерфейс, внешний файл и так далее.

Метод использования также очень прост, вы можете обратиться к единственному тесту проекта:

    @Test
    public void tryLock() throws Exception {
        String key = "test";
        String request = UUID.randomUUID().toString();
        Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");

        boolean locktest = redisLock.tryLock(key, request);
        System.out.println("locktest=" + locktest);

        Assert.assertTrue(locktest);

        //check
        Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong());
    }

Это всего лишь краткая демонстрация, если возможно, внимательно проанализируйте ее в следующий раз.

Его принцип на самом деле довольно прост, и его можно увидеть непосредственно при отладке:

JedisCluster, на который мы здесь полагаемся, на самом деле являетсяcglib 代理对象. Так что не сложно понять, как это работает.

Например, здесь нам нужно использовать функцию набора JedisCluster и нам нужно ее возвращаемое значение.

Mock проксирует объект и возвращает вам пользовательское значение после фактического выполнения метода set.

Так что мы можем тестировать все, что захотим,Полностью защищен от внешних зависимостей.

Суммировать

На данный момент реализована распределенная блокировка на базе Redis, но есть еще некоторые проблемы.

  • Например, после истечения срока действия ключа бизнес не завершается, но блокировка автоматически снимается, что приведет к проблемам параллелизма.
  • Даже если Redis развернут в кластере, если каждый узел является только мастером без подчиненного, то все ключи на узле, когда мастер выходит из строя, эквивалентны снятию блокировки в этот момент, что также вызовет проблемы параллелизма. Даже при наличии подчиненного узла описанная выше проблема возникнет, если ведущий выйдет из строя до того, как данные будут синхронизированы с подчиненным.

Заинтересованные друзья также могут обратиться кRedissonреализация.

Дополнительный

Недавно я обобщил некоторые знания, связанные с Java, и заинтересованные друзья могут поддерживать их вместе.

адрес:GitHub.com/crossover J я…