Переделывать всегда проще, чем дооснащать
Недавно я работал над проектом по внедрению системы от другой компании (в дальнейшем именуемая старая система), полностью интегрированная в систему собственной компании (именуемая в дальнейшем как новая система), в котором функции, реализованные другой стороной, должны быть полностью реализованы в их собственной системе.
Есть ряд старых бизнес-систем инвентаризации, чтобы не влиять на запасы делового опыта, новая система предоставляет внешние интерфейсы, которые также должны соответствовать предыдущим версиям. Наконец, после полного переключения системные функции выполняются только в новой системе, для которой требуются данные из старой системы, необходимые для полной миграции в новую систему.
Конечно, они ожидали до этого проекта. Я думал, что этот процесс будет трудным, но я не ожидал, что это будет так сложно. Первоначально я чувствовал, что расписание было более полугода, и было еще много времени. Теперь ощущается, как большая дыра, и я должен вполнить его немного.
Эй, в основном это слезы, так что я не буду жаловаться, я дам тебе реальный опыт и опыт, когда я закончу это в следующий раз.
Вернуться к основному тексту, предыдущая статьяРаспределенная блокировка Redis, мы реализуем распределенную блокировку на основе Redis. С базовыми функциями этой распределенной блокировки проблем нет, но ей не хватает реентерабельных функций, поэтому в этой статье Сяо Хей предложит вам реализовать реентерабельную распределенную блокировку.
В этой статье будет рассмотрено следующее:
- возвращающийся
- Реализация ThreadLocal.
- Схема реализации на основе Redis Hash
Сначала лайкните, а потом смотрите, сделайте это привычкой. Wechat ищет «Программное общение», следуйте ему, и все готово ~
возвращающийся
Говоря о повторных блокировках, сначала давайте посмотрим на абзац изwikiПовторное объяснение выше:
Если программу или подпрограмму можно «прервать в любое время, и операционная система планирует выполнение другого фрагмента кода, этот код вызывает подпрограмму без ошибок», то она называетсявозвращающийся(Reentrant или Re-entRant). То есть, когда подпрограмма выполняется, поток выполнения может войти в нее и выполнить ее снова, все еще получая результаты, ожидаемые при разработке проекта. В отличие от многопоточных параллельных потоков, повторный вход в одну и ту же подпрограмму при выполнении одного потока по-прежнему безопасен.
Когда поток выполняет часть кода и успешно получает блокировку и продолжает выполняться, он снова сталкивается с заблокированным кодом.Повторный вход гарантирует, что поток может продолжать выполняться, в то время как неповторный вход означает, что ему нужно дождаться снятия блокировки. освобожден, а затем снова успешно получить блокировку, чтобы продолжить выполнение.
Объясните повторный вход с помощью фрагмента кода Java:
public synchronized void a() {
b();
}
public synchronized void b() {
// pass
}
Предполагая, что поток X продолжает выполнять метод b после того, как метод a получает блокировку, если в это времяне реентерабельный, поток должен дождаться освобождения блокировки, а затем снова конкурировать за блокировку.
Блокировка явно принадлежит потоку X, но ему все равно нужно дождаться, пока блокировка освободится сама, а затем захватить блокировку, что выглядит очень странно, я освобождаюсь~
Реентерабельность может решить эту неловкую проблему: после того, как поток получил блокировку, он встречает метод блокировки в будущем, напрямую увеличивает количество блокировок на 1, а затем выполняет логику метода. После выхода из метода блокировки количество блокировок уменьшается на 1. Когда количество блокировок равно 0, блокировка действительно снимается.
Можно заметить, что самой большой особенностью повторных блокировок является подсчет, который подсчитывает количество блокировок. Следовательно, когда в распределенной среде необходимо реализовать реентерабельные блокировки, нам также необходимо подсчитать количество блокировок.
Существует два способа реализации распределенных блокировок с повторным входом:
- Схема реализации на основе ThreadLocal
- Схема реализации на основе Redis Hash
Сначала мы видим схему реализации ThreadLocal.
Схема реализации на основе ThreadLocal
Метод реализации
на ЯвеThreadLocal
У каждого потока может быть своя собственная копия экземпляра, и мы можем использовать эту функцию для выполнения методов повторного входа в поток.
Ниже мы определяемThreadLocal
глобальная переменнаяLOCKS
, памятьMap
Переменные экземпляра.
private static ThreadLocal<Map<String, Integer>> LOCKS = ThreadLocal.withInitial(HashMap::new);
Каждый поток может пройтиThreadLocal
получить свой собственныйMap
пример,Map
серединаkey
хранит имя блокировки, аvalue
Количество повторных входов для сохранения блокировки.
Код блокировки выглядит следующим образом:
/**
* 可重入锁
*
* @param lockName 锁名字,代表需要争临界资源
* @param request 唯一标识,可以使用 uuid,根据该值判断是否可以重入
* @param leaseTime 锁释放时间
* @param unit 锁释放时间单位
* @return
*/
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
Map<String, Integer> counts = LOCKS.get();
if (counts.containsKey(lockName)) {
counts.put(lockName, counts.get(lockName) + 1);
return true;
} else {
if (redisLock.tryLock(lockName, request, leaseTime, unit)) {
counts.put(lockName, 1);
return true;
}
}
return false;
}
ps:
redisLock#tryLock
Чтобы добиться блокировки на распространение статьи.Поскольку по внешней ссылке общедоступного номера нельзя напрямую перейти, обратите внимание на «общая программа",ОтветитьРаспределенная блокировкаПолучите исходный код.
Метод блокировки сначала определяет, принадлежит ли текущий поток замком, и, если это так, напрямую добавляет 1 на количество блокировок ReentRies.
Если у вас еще нет замка, попробуйтеRedisБлокировка, после успешной блокировки добавляем 1 к количеству повторных входов.
Код для снятия блокировки выглядит следующим образом:
/**
* 解锁需要判断不同线程池
*
* @param lockName
* @param request
*/
public void unlock(String lockName, String request) {
Map<String, Integer> counts = LOCKS.get();
if (counts.getOrDefault(lockName, 0) <= 1) {
counts.remove(lockName);
Boolean result = redisLock.unlock(lockName, request);
if (!result) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
+ request);
}
} else {
counts.put(lockName, counts.get(lockName) - 1);
}
}
При освобождении блокировки сначала определите количество повторных входов, если оно больше 1, это означает, что блокировка принадлежит потоку, поэтому напрямую уменьшите количество повторных входов на 1.
Если текущее количество повторных входов меньше или равно 1, удалите сначалаMap
Заблокируйте соответствующий ключ, а затем перейдите в Redis, чтобы снять блокировку.
Следует отметить, что, когда поток владеет блокировкой, не напрямую разблокирует, может также повторно ввести число меньше или равно 1, это может быть невозможно разблокировать немедленный успех.
ThreadLocal
Не забывайте вовремя очищать переменные экземпляра внутреннего хранилища, чтобы предотвратить утечку памяти, использование строк контекстных данных и т. д.В следующий раз поговорим о недавнем использовании
ThreadLocal
Ошибка написана.
Похожие вопросы
использоватьThreadLocal
Хотя эта локальная запись времени повторного входа действительно проста и эффективна, есть и некоторые проблемы.
Проблема со сроком годности
Как видно из приведенного выше кода блокировки, когда повторный вход заблокирован, к локальному счету добавляется только 1. Это может привести к ситуации, когда срок действия Redis истек для снятия блокировки из-за длительного выполнения бизнес-процессов.
При повторном входе в блокировку, поскольку данные все еще есть локально, считается, что блокировка все еще удерживается, что не соответствует реальной ситуации.
Если вы хотите увеличить время истечения локально, вам также необходимо учитывать согласованность времени истечения между локальным и Redis, и код станет очень сложным.
Другая проблема повторного входа потока/процесса
Реентерабельность в узком смысле следует использовать только длятот же потокВоспроизведение, но реальный бизнес может потребовать, чтобы разные потоки приложений повторно вводили одну и ту же блокировку.
иThreadLocal
Решение может удовлетворить только повторный вход в один и тот же поток и не может решить проблему повторного входа между разными потоками/процессами.
Проблема повторного входа различных потоков/процессов должна быть решена с помощью следующего решения Redis Hash.
Реентерабельная блокировка на основе Redis Hash
Метод реализации
ThreadLocal
В схеме мы использовалиMap
Запишите, сколько раз блокировка может быть повторно введена, и Redis также предоставляет Hash (хеш-таблицу) — структуру данных, в которой могут храниться пары «ключ-значение». Таким образом, мы можем использовать время повторного входа блокировки, хранящейся в Redis Hash, а затем использоватьlua
Скрипта суждения логики.
Заблокированный скрипт lua выглядит следующим образом:
---- 1 代表 true
---- 0 代表 false
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end ;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end ;
return 0;
если КЛЮЧИ:[замок],ARGV[1000,uuid]
Не бойтесь, если вы не знакомы с языком lua, приведенная выше логика относительно проста.
Код блокировки сначала использует Redisexists
Команда определяет, существует ли текущая блокировка.
Если блокировки не существует, используйте ее напрямуюhincrby
Создать ключ дляlock
хэш-таблица, а ключ в хеш-таблицеuuid
Инициализируйте до 0, затем снова добавьте 1 и, наконец, установите время истечения срока действия.
Если текущая блокировка существует, используйтеhexists
судить о текущемlock
Существует ли соответствующая хеш-таблицаuuid
Этот ключ, если он присутствует, используется сноваhincrby
Плюс 1, и, наконец, снова установил время истечения срока действия.
Наконец, если две приведенные выше логики не совпадают, вернитесь напрямую.
Код блокировки выглядит следующим образом:
// 初始化代码
String lockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:lock.lua").openStream(), Charsets.UTF_8);
lockScript = new DefaultRedisScript<>(lockLuaScript, Boolean.class);
/**
* 可重入锁
*
* @param lockName 锁名字,代表需要争临界资源
* @param request 唯一标识,可以使用 uuid,根据该值判断是否可以重入
* @param leaseTime 锁释放时间
* @param unit 锁释放时间单位
* @return
*/
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
long internalLockLeaseTime = unit.toMillis(leaseTime);
return stringRedisTemplate.execute(lockScript, Lists.newArrayList(lockName), String.valueOf(internalLockLeaseTime), request);
}
Spring-Boot 2.2.7.RELEASE
Если вы понимаете логику блокировки сценария Lua, реализация кода Java довольно проста, и вы можете напрямую использовать код, предоставляемый SpringBoot.StringRedisTemplate
Вот и все.
Разблокированный сценарий Lua выглядит следующим образом:
-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 0 代表 该可重入 key 不存在
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
end ;
-- 计算当前可重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 小于等于 0 代表可以解锁
if (counter > 0) then
return 0;
else
redis.call('del', KEYS[1]);
return 1;
end ;
return nil;
первое использованиеhexists
Анализ хеш-таблицы Redis хранится в заданном домене.
Если хэш-таблица, соответствующая блокировке, не существует или ключ uuid не существует в хэш-таблице, возвращайтесь напрямую.nil
.
Если он существует, это означает, что текущая блокировка удерживается им, сначала используйтеhincrby
Уменьшите количество повторных входов на 1, а затем оцените количество повторных входов после расчета.Если оно меньше или равно 0, используйтеdel
Снимите этот замок.
Разблокированный код Java выглядит следующим образом:
// 初始化代码:
String unlockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:unlock.lua").openStream(), Charsets.UTF_8);
unlockScript = new DefaultRedisScript<>(unlockLuaScript, Long.class);
/**
* 解锁
* 若可重入 key 次数大于 1,将可重入 key 次数减 1 <br>
* 解锁 lua 脚本返回含义:<br>
* 1:代表解锁成功 <br>
* 0:代表锁未释放,可重入次数减 1 <br>
* nil:代表其他线程尝试解锁 <br>
* <p>
* 如果使用 DefaultRedisScript<Boolean>,由于 Spring-data-redis eval 类型转化,<br>
* 当 Redis 返回 Nil bulk, 默认将会转化为 false,将会影响解锁语义,所以下述使用:<br>
* DefaultRedisScript<Long>
* <p>
* 具体转化代码请查看:<br>
* JedisScriptReturnConverter<br>
*
* @param lockName 锁名称
* @param request 唯一标识,可以使用 uuid
* @throws IllegalMonitorStateException 解锁之前,请先加锁。若为加锁,解锁将会抛出该错误
*/
public void unlock(String lockName, String request) {
Long result = stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
// 如果未返回值,代表其他线程尝试解锁
if (result == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
+ request);
}
}
Режим выполнения кода разблокировки аналогичен блокировке, но только результат выполнения разблокировки возвращается к типуLong
. Причина здесь не в том, чтобы использовать заблокированныйBoolean
, это связано с тем, что в lua-скрипте разблокировки три возвращаемых значения имеют следующие значения:
- 1 означает, что разблокировка прошла успешно и блокировка снята.
- 0 означает, что количество повторных входов уменьшается на 1
-
null
Попытка разблокировки от имени других потоков, разблокировка не удалась
Если используется возвращаемое значениеBoolean
,Spring-data-redisКогда будет преобразование типаnull
Обратимся к false, что повлияет на наше логическое суждение, поэтому необходимо использовать возвращаемый типLong
.
Следующий код взят изJedisScriptReturnConverter
:
Похожие вопросы
Проблема с низкой версией Spring-data-redis
Если Spring-Boot использует Jedis в качестве клиента подключения и использует режим кластера Redis Cluster, вам необходимо использовать2.1.9версия вышеspring-boot-starter-data-redis, иначе процесс выполнения выдаст:
org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.
Если текущее приложение не может быть обновленоspring-data-redis
Это не имеет значения, вы можете использовать следующий метод, чтобы напрямую использовать собственное соединение Jedis для выполнения сценариев lua.
В качестве примера возьмем код блокировки:
public boolean tryLock(String lockName, String reentrantKey, long leaseTime, TimeUnit unit) {
long internalLockLeaseTime = unit.toMillis(leaseTime);
Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
Object innerResult = eval(connection.getNativeConnection(), lockScript, Lists.newArrayList(lockName), Lists.newArrayList(String.valueOf(internalLockLeaseTime), reentrantKey));
return convert(innerResult);
});
return result;
}
private Object eval(Object nativeConnection, RedisScript redisScript, final List<String> keys, final List<String> args) {
Object innerResult = null;
// 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群
if (nativeConnection instanceof JedisCluster) {
innerResult = evalByCluster((JedisCluster) nativeConnection, redisScript, keys, args);
}
// 单点
else if (nativeConnection instanceof Jedis) {
innerResult = evalBySingle((Jedis) nativeConnection, redisScript, keys, args);
}
return innerResult;
}
Проблема преобразования типа данных
Если вы используете собственное соединение Jedis для выполнения сценариев Lua, вы можете снова столкнуться с ямами преобразования типов данных.
можно увидетьJedis#eval
возвращениеObject
, нам нужно выполнить соответствующие преобразования в соответствии с возвращаемым значением скрипта Lua. Это включает в себя преобразование типов данных Lua в типы данных Redis.
Давайте поговорим о нескольких правилах преобразования данных Lua в Redis, на которые проще наступить:
1. Преобразование числа Lua и типа данных Redis
В числовом типе Lua используется двойная точность с плавающей запятой, но Redis поддерживает только целые типы, в процессе преобразования десятичные числа будут отброшены.
2. Преобразование типа Lua Boolean и Redis
Это преобразование легче наступить на яму, в Redis нет логического типа, поэтому когда Luatrue
будет преобразовано в целое число Redis 1. И в Луаfalse
Это не преобразование целых чисел, это преобразованиеnullвозвращен клиенту.
3. Преобразование типов Lua nil и Redis
Lua Nil можно рассматривать как нулевое значение, которое может быть эквивалентно Javanull. В Lua, если в условном выражении появляется nil, оно будет считаться ложным.
Так что Lua nil тоже будетnullвозвращен клиенту.
Другие правила преобразования относительно просты, см.:
doc.Redis fan.com/script/eval…
Суммировать
Ключом к повторному входу в распределенные блокировки является количество повторных входов в блокировку.В этой статье в основном представлены два решения, одно из которых основано наThreadLocal
Реализация этого решения проста, а работа также более эффективна. Но если вы хотите обработать истечение срока действия блокировки, реализация кода будет более сложной.
Другая схема реализации с использованием структуры данных Redis Hash решает проблему.ThreadLocal
Однако реализовать код немного сложнее, и вам необходимо быть знакомым со сценариями Lua и некоторыми командами Redis. дополнительно использоватьspring-data-redisПри работе с Redis вы случайно столкнетесь с различными проблемами.
помощь
Wooooooo.so post stack.specialty/blog/sofa-just…
Специальности.Meituan.com/2016/09/29/…
Наконец, два слова (пожалуйста, обратите внимание)
Прочитав статью, братья и сестры, нажмитеотличныйЧто ж, Чжоу Гэн действительно устал и писал еще два дня, сам того не осознавая.
Наконец, спасибо за ваше чтение.Есть неизбежные ошибки.Если вы обнаружите какие-либо ошибки, вы можете оставить сообщение, чтобы указать. Если есть что-то еще, что вы не поняли после прочтения статьи, пожалуйста, добавьте меня, учитесь друг у друга и развивайтесь вместе~
Наконец, спасибо за вашу поддержку ~
Последнее, но не менее важное, еще одна важная вещь ~
Приходи и следуй за мной~
Приходи и следуй за мной~
Приходи и следуй за мной~
Добро пожаловать, чтобы обратить внимание на мой официальный аккаунт: программа для общения, ежедневный толчок галантерейных товаров. Если вас интересует мой рекомендуемый контент, вы также можете подписаться на мой блог:studyidea.cn