Практика реализации распределенной блокировки на основе Redis в Node.js

Node.js

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

об авторе: Джун Май, разработчик Nodejs, сертифицированный автор МООК, молодежь после 90-х, которая любит технологии и любит делиться, добро пожаловать на внимание.Стек технологий Nodejsи проект с открытым исходным кодом Githubwww.nodejs.red

Понимание потоков, процессов, распределенных блокировок

замок резьбы: запросы выполняются последовательно в однопоточном режиме программирования. Одно из преимуществ заключается в том, что не нужно учитывать вопросы безопасности потоков и конкуренции за ресурсы, поэтому при программировании Node.js вы не будете учитывать вопросы безопасности потоков. В многопоточном режиме программирования, таком как Java, вы можете быть знакомы со словом «синхронизированный», которое обычно является самым простым способом решения параллельного программирования в Java.Синхронизация может гарантировать, что только один поток выполняет метод или блок кода в то же время. .

блокировка процесса: служба развернута на сервере, и одновременно открыто несколько процессов. Чтобы использовать ресурсы операционной системы при программировании Node.js, можно включить многопроцессный режим в зависимости от количества ядер ЦП. В настоящее время , если вы работаете с общим ресурсом, вы все равно столкнетесь с ресурсами.Проблема конкуренции, кроме того, каждый процесс независим друг от друга и имеет свое собственное независимое пространство памяти. Также сложно разрешить блокировки процессов через синхронизацию в Java, так как синхронизация действительна только в той же JVM.

Распределенная блокировка: Независимо от того, находится ли служба в однопоточном или многопроцессорном режиме, она все равно столкнется с одной и той же проблемой, когда будет развернута на нескольких компьютерах и будет работать с одним и тем же общим ресурсом в распределенной среде. На этом этапе необходимо ввести понятие распределенной блокировки. Как показано на рисунке ниже, поскольку операция SET выполняется после того, как бизнес-логика модифицирует первые считанные данные, это не атомарная операция.Введение распределенных блокировок для решения обычно является очень широким решением.

图片描述

Идея реализации распределенной блокировки на базе Redis

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

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

тупик: Взаимная блокировка может быть вызвана какой-либо причиной, блокировка, которая должна быть снята, не была снята, поэтому время истечения может быть установлено синхронно при блокировке.Если она не снята по собственным причинам клиента, также необходимо обеспечить что блокировка может быть автоматически разблокирована.

Отказоустойчивость: в многоузловом режиме необходимо учитывать отказоустойчивость.Пока гарантируется доступность N/2+1 узлов, клиент может успешно получать и снимать блокировки.

Реализация распределенной блокировки Redis с одним экземпляром

Простая распределенная блокировка реализована под экземпляром Redis с одним узлом. Здесь для достижения атомарности будут использоваться несколько простых Lua-скриптов. Если вы не понимаете, вы можете обратиться к предыдущей статье.Практика написания сценариев Redis Lua в Node.js

заблокирован

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

setnx key value
expire key seconds

Вышеупомянутая команда не является атомарной операцией.Так называемая атомарная операция означает, что команда не будет прервана другими потоками или запросами в процессе выполнения.Если указанный выше setnx успешно выполнен, команда network flash expires не будет выполнена. т.е приведет к тупику.

Вы можете думать об использовании вещей для решения проблемы, но у вещей есть характеристика, успех или неудача, все они выполняются на одном дыхании.В нашем примере выше, expire нужно сначала решить, нужно ли его устанавливать в соответствии с результатом. of setnx, очевидно, что здесь ничего не работает, и в сообществе есть много библиотек для решения этой проблемы.Теперь, после официальной версии 2.8 Redis, он поддерживает команду set для передачи расширенных параметров setnx и истечения срока действия, так что одна команда может быть выполнена за один раз, что позволяет избежать вышеперечисленных проблем.

  • Значение: Рекомендуемая настройка - это случайное значение, будет дополнительно объяснено в времени выпуска блокировки
  • EX секунд: установить время истечения
  • PX миллисекунды: также установите время истечения срока действия, единицы измерения разные
  • NX|XX: NX имеет тот же эффект, что и setnx.
set key value [EX seconds] [PX milliseconds] [NX|XX]

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

Процесс снятия блокировки заключается в удалении изначально занятой ямы, но нельзя просто с помощью клавиши del удалить ее и все будет хорошо.Удалять чужие блокировки легко, почему? Например, клиент А получает блокировку с ключом = имя1 (в течение 2 секунд), а затем обрабатывает свою собственную бизнес-логику.Однако, когда блок обработки бизнес-логики занимает больше времени, чем блокировка, блокировка автоматически снимается.При этом период, когда ресурс приобретается клиентом B с блокировкой key = name1, затем клиент A напрямую удаляет блокировку с помощью команды del key после завершения своей бизнес-обработки, которая снимает блокировку клиента B. Следовательно, при снятии блокировки вы должны снимать только тот замок, которым владеете.

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

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Практика распределенной блокировки Redis с одним экземпляром Node.js

Клиент Redis, использующий Node.js, называется ioredis, а npm install ioredis -S сначала устанавливает пакет.

Инициализировать пользовательский RedisLock

class RedisLock {
    /**
     * 初始化 RedisLock
     * @param {*} client 
     * @param {*} options 
     */
    constructor (client, options={}) {
        if (!client) {
            throw new Error('client 不存在');
        }

        if (client.status !== 'connecting') {
            throw new Error('client 未正常链接');
        }

        this.lockLeaseTime = options.lockLeaseTime || 2; // 默认锁过期时间 2 秒
        this.lockTimeout = options.lockTimeout || 5; // 默认锁超时时间 5 秒
        this.expiryMode = options.expiryMode || 'EX';
        this.setMode = options.setMode || 'NX';
        this.client = client;
    }
}

заблокирован

Передайте расширенные параметры setnx и expire через команду set, чтобы начать блокировку и занятие ямы. Блокировка успешно возвращена, и блокировка не может быть повторена. Если блокировка не получена в течение времени, указанного lockTimeout, блокировка не будет полученный.

class RedisLock {
    
    /**
     * 上锁
     * @param {*} key 
     * @param {*} val 
     * @param {*} expire 
     */
    async lock(key, val, expire) {
        const start = Date.now();
        const self = this;

        return (async function intranetLock() {
            try {
                const result = await self.client.set(key, val, self.expiryMode, expire || self.lockLeaseTime, self.setMode);
        
                // 上锁成功
                if (result === 'OK') {
                    console.log(`${key} ${val} 上锁成功`);
                    return true;
                }

                // 锁超时
                if (Math.floor((Date.now() - start) / 1000) > self.lockTimeout) {
                    console.log(`${key} ${val} 上锁重试超时结束`);
                    return false;
                }

                // 循环等待重试
                console.log(`${key} ${val} 等待重试`);
                await sleep(3000);
                console.log(`${key} ${val} 开始重试`);

                return intranetLock();
            } catch(err) {
                throw new Error(err);
            }
        })();
    }
}

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

Снимите блокировку, выполнив сценарий redis lua, который мы определили с помощью redis.eval(script).

class RedisLock {
    /**
     * 释放锁
     * @param {*} key 
     * @param {*} val 
     */
    async unLock(key, val) {
        const self = this;
        const script = "if redis.call('get',KEYS[1]) == ARGV[1] then" +
        "   return redis.call('del',KEYS[1]) " +
        "else" +
        "   return 0 " +
        "end";

        try {
            const result = await self.client.eval(script, 1, key, val);

            if (result === 1) {
                return true;
            }
            
            return false;
        } catch(err) {
            throw new Error(err);
        }
    }
}

тестовое задание

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

const Redis = require("ioredis");
const redis = new Redis(6379, "127.0.0.1");
const uuidv1 = require('uuid/v1');
const redisLock = new RedisLock(redis);

function sleep(time) {
    return new Promise((resolve) => {
        setTimeout(function() {
            resolve();
        }, time || 1000);
    });
}

async function test(key) {
    try {
        const id = uuidv1();
        await redisLock.lock(key, id, 20);
        await sleep(3000);
        
        const unLock = await redisLock.unLock(key, id);
        console.log('unLock: ', key, id, unLock);
    } catch (err) {
        console.log('上锁失败', err);
    }  
}

test('name1');
test('name1');

При этом дважды вызывался тестовый метод на блокировку.Успешно прошел только первый.При блокировке второго name1 26e02970-0532-11ea-b978-2160dffafa30 было обнаружено, что key=name1 был занят и начал В приведенном выше тесте блокировка автоматически снимается через 3 секунды, а имя1 26e02970-0532-11ea-b978-2160dffafa30 успешно блокируется после двух попыток.

name1 26e00260-0532-11ea-b978-2160dffafa30 上锁成功
name1 26e02970-0532-11ea-b978-2160dffafa30 等待重试
name1 26e02970-0532-11ea-b978-2160dffafa30 开始重试
name1 26e02970-0532-11ea-b978-2160dffafa30 等待重试
unLock:  name1 26e00260-0532-11ea-b978-2160dffafa30 true
name1 26e02970-0532-11ea-b978-2160dffafa30 开始重试
name1 26e02970-0532-11ea-b978-2160dffafa30 上锁成功
unLock:  name1 26e02970-0532-11ea-b978-2160dffafa30 true

Адрес источника

https://github.com/Q-Angelo/project-training/tree/master/redis/lock/redislock.js

Алгоритм Редлока

Выше приведена простая реализация распределенной блокировки Redis с использованием Node.js, которая доступна в одном экземпляре Когда мы расширим узел Redis, что произойдет в Sentinel и Redis Cluster?

Ниже приведен пример схемы автоматического аварийного переключения Redis Sentinel. Предположим, что после того, как наш клиент A получает блокировку на главном узле 192.168.6.128, главный узел зависает до синхронизации информации с подчиненным узлом.В это время Sentinel будет выбрать другой.Поскольку ведомый узел является главным узлом, то клиент B также подает заявку на ту же блокировку в это время, и одна и та же блокировка будет удерживаться несколькими клиентами.Недостаточно предъявлять высокие требования к окончательной согласованности данные.

图片描述

Представляем Редлок

Ввиду этих проблем официальный сайт RedisRedis.IO/topics/ День 3…предоставилКанонический алгоритм реализации распределенных блокировок с помощью Redis Redlock, ссылка на китайскую версию переводаRedis.capable/topics/День 3…

Redlock также описан в вышеупомянутых документах. Вот краткое резюме: Redlock обеспечивает надежные гарантии в одном или нескольких экземплярах Redis и обладает отказоустойчивостью. Он будет использовать один и тот же ключ и случайное значение из N экземпляров, чтобы попытатьсяset key value [EX seconds] [PX milliseconds] [NX|XX]Команда для получения блокировки. Если не менее N/2+1 экземпляров Redis получают блокировку в течение допустимого времени, получение блокировки считается успешным. В противном случае получение блокировки завершается сбоем. В случае сбоя клиент должен разблокировать все экземпляры Redis.

Применение Redlock в Node.js

GitHub.com/Мик-Марк AC…Это версия реализации Redlock для Node.js. Она также очень проста в использовании. Перед запуском установите пакеты ioredis и redlock.

npm i ioredis -S
npm i redlock -S

кодирование

const Redis = require("ioredis");
const client1 = new Redis(6379, "127.0.0.1");
const Redlock = require('redlock');
const redlock = new Redlock([client1], {
    retryDelay: 200, // time in ms
    retryCount: 5,
});

// 多个 Redis 实例
// const redlock = new Redlock(
//     [new Redis(6379, "127.0.0.1"), new Redis(6379, "127.0.0.2"), new Redis(6379, "127.0.0.3")],
// )

async function test(key, ttl, client) {
    try {
        const lock = await redlock.lock(key, ttl);

        console.log(client, lock.value);
        // do something ...

        // return lock.unlock();
    } catch(err) {
        console.error(client, err);
    }
}

test('name1', 10000, 'client1');
test('name1', 10000, 'client2');

тестовое задание

Один и тот же ключ name1 блокируется дважды, так как client1 первым получил блокировку, client2 не может получить блокировку, и после 5 повторных попыток сообщается об ошибке LockError: Exceeded 5 попыток заблокировать ресурс "name1".

图片描述