Объем, который может нести одна машина, ограничен, а количество пользователей составляет порядка десятков тысяч.В основном сервисы будут развертываться в распределенных кластерах. Много раз вы будете сталкиваться с методами на одном и том же ресурсе. В настоящее время необходимы блокировки.Если это автономная версия, вы можете использовать параллельную обработку синхронизации, которая поставляется с такими языками, как java. Если он развернут на нескольких машинах, должен быть промежуточный агент для выполнения распределенных блокировок.
Существует три способа реализации часто используемых распределенных блокировок.
- На основе реализации redis (реализовано атомарной операцией redis setnx)
- На основе реализации mysql (используя блокировку строки mysql innodb для достижения, есть два способа: пессимистическая блокировка и оптимистическая блокировка)
- На основе реализации Zookeeper (реализовано с использованием временных последовательных узлов zk)
В настоящее время я реализовал блокировки с помощью Redis и MySQL и применил их в различных онлайн-средах в соответствии со сценариями приложений. Реализация zk относительно сложна и не имеет сценариев применения.Если вам интересно, вы можете обратиться к «Zokeeper Implementing Distributed Locks» в Stone of the Mountain.
Расскажите о своих мыслях и переживаниях.
Нет идеальной технологии, нет мастер-ключа, разные способы и разные сценарии применения Принцип CAP: непротиворечивость, доступность и устойчивость к разделам, выберите два.
Камень других гор
- Zookeeper реализует распределенные блокировки:woohoo.brief.com/afraid/5's 12ah 0101…
- Несколько реализаций распределенных замков~:Вуху. Hollisbeds.com/archives/17…
- select for update вызывает анализ взаимоблокировок:блог woo woo woo.cn на.com/Micra Day/Afraid/8…
Распределенная блокировка на основе кеша Redis
Реализация блокировки на основе Redis относительно проста. Поскольку выполнение Redis является однопоточным выполнением, оно, естественно, является атомарным. Мы можем использовать команды setnx и expire для его достижения. Ссылка на код версии Java выглядит следующим образом:
package com.fenqile.creditcard.appgatewaysale.provider.util;
import com.fenqile.redis.JedisProxy;
import java.util.Date;
/**
* User: Rudy Tan
* Date: 2017/11/20
*
* redis 相关操作
*/
public class RedisUtil {
/**
* 获取分布式锁
*
* @param key string 缓存key
* @param expireTime int 过期时间,单位秒
* @return boolean true-抢到锁,false-没有抢到锁
*/
public static boolean getDistributedLockSetTime(String key, Integer expireTime) {
try {
// 移除已经失效的锁
String temp = JedisProxy.getMasterInstance().get(key);
Long currentTime = (new Date()).getTime();
if (null != temp && Long.valueOf(temp) < currentTime) {
JedisProxy.getMasterInstance().del(key);
}
// 锁竞争
Long nextTime = currentTime + Long.valueOf(expireTime) * 1000;
Long result = JedisProxy.getMasterInstance().setnx(key, String.valueOf(nextTime));
if (result == 1) {
JedisProxy.getMasterInstance().expire(key, expireTime);
return true;
}
} catch (Exception ignored) {
}
return false;
}
}
Просто замените имя пакета и получите объект операции Redis своим собственным.
Основные шаги
- Каждый раз, когда заходите, проверяйте, реализован ли ключ. Если это не удается, удалите недействительную блокировку.
- Используйте атомарную команду setnx для захвата блокировок.
- Установленное время истечения блокировки захвата.
Шаг 2 - это самое главное, Зачем ставить шаг 3? Для потока, получившего блокировку, может быть запрос на удаление, но блокировка не может быть снята, поэтому установите максимальное время блокировки, чтобы избежать взаимоблокировки. Зачем ставить шаг 1? Redis может зависнуть при истечении срока действия настройки. Установка времени истечения не удалась, и блокировка, похоже, действует постоянно.
В онлайн-среде возникли проблемы шагов 1 и 3. Так что обязательно перехватите.
развертывание кластера Redis
Обычно Redis решает одноточечные проблемы с master-slave.Несколько master-slaves образуют большой кластер, а затем направляют разные ключи на разные master-slave узлы с помощью последовательного алгоритма хеширования.
Преимущества и недостатки блокировки Redis:
Преимущества: Redis сам по себе является операцией с памятью и обычно развертывается несколькими частями, поэтому он имеет такой высокий контроль параллелизма и может выдерживать большое количество запросов. Недостатки: Redis сам по себе является кешем, и есть определенная вероятность запросов о несоответствии данных.
Раньше в онлайне, используя Redis в качестве счетчика инвентаря, теоретически раздавали только 10 призов, а окончательно раздали 14. Есть проблема согласованности данных.
Поэтому после этого была введена распределенная блокировка базы данных mysql.
Распределенная блокировка на базе MySQL.
Реализовать первую редакцию
До этого перерыл много статей в интернете, в основном все они про вставку, удаление или прямое получение блокировок и счетчиков в виде "выбрать для обновления". За подробностями обратитесь к главе о блокировках базы данных в «Несколько реализаций распределенных блокировок~» в Stone of the Mountain.
В начале псевдокод моей реализации выглядит следующим образом:
public boolean getLock(String key){
select for update
if (记录存在){
update
}else {
insert
}
}
Таким образом, возникает серьезная проблема взаимоблокировки.По конкретным причинам вы можете обратиться к разделу «Выбрать для анализа взаимоблокировок причин обновления» в Stone of the Mountain. В этой версии есть несколько серьезных проблем:
1. Обычно онлайн-данные не разрешается физически удалять 2. Нецелесообразно повторно обрабатывать форму ошибки через уникальный ключ. 3. Если клиент-приложение зависает до снятия блокировки во время обработки, блокировка всегда будет существовать и возникнет взаимоблокировка. 4. Если счетчик (incr decr) в redis реализовать таким образом, то при отсутствии записи будет много взаимоблокировок.
Поэтому рассмотрите возможность введения понятия поля состояния записи и центрального замка.
Реализовать второе издание
Во втором издании дизайн таблицы базы данных был улучшен, и ссылка выглядит следующим образом:
-- 锁表,单库单表
CREATE TABLE IF NOT EXISTS credit_card_user_tag_db.t_tag_lock (
-- 记录index
Findex INT NOT NULL AUTO_INCREMENT COMMENT '自增索引id',
-- 锁信息(key、计数器、过期时间、记录描述)
Flock_name VARCHAR(128) DEFAULT '' NOT NULL COMMENT '锁名key值',
Fcount INT NOT NULL DEFAULT 0 COMMENT '计数器',
Fdeadline DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '锁过期时间',
Fdesc VARCHAR(255) DEFAULT '' NOT NULL COMMENT '值/描述',
-- 记录状态及相关事件
Fcreate_time DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间',
Fmodify_time DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '修改时间',
Fstatus TINYINT NOT NULL DEFAULT 1 COMMENT '记录状态,0:无效,1:有效',
-- 主键(PS:总索引数不能超过5)
PRIMARY KEY (Findex),
-- 唯一约束
UNIQUE KEY uniq_Flock_name(Flock_name),
-- 普通索引
KEY idx_Fmodify_time(Fmodify_time)
)ENGINE=INNODB DEFAULT CHARSET=UTF8 COMMENT '信用卡|锁与计数器表|rudytan|20180412';
В этой версии концепция центральной блокировки введена с учетом тупиковой ситуации (конфликт за блокировку промежутка) при параллельной вставке блокировки.
Основной способ:
- Создать базу данных на основе sql
- Создайте запись с Flock_name="center_lock".
- При работе с другими замками (такими как Flock_name="sale_invite_lock") сначала выберите запись для обновления на "center_lock"
- «sale_invite_lock» записывает собственные добавления, удаления и изменения.
Учитывая, что пакеты операций с базами данных, представленные разными компаниями, различаются, для облегчения понимания предоставляется псевдокод. Поддельный код
// 开启事务
@Transactional
public boolean getLock(String key){
// 获取中央锁
select * from tbl where Flock_name="center_lock"
// 查询key相关记录
select for update
if (记录存在){
update
}else {
insert
}
}
/**
* 初始化记录,如果有记录update,如果没有记录insert
*/
private LockRecord initLockRecord(String key){
// 查询记录是否存在
LockRecord lockRecord = lockMapper.queryRecord(key);
if (null == lockRecord) {
// 记录不存在,创建
lockRecord = new LockRecord();
lockRecord.setLockName(key);
lockRecord.setCount(0);
lockRecord.setDesc("");
lockRecord.setDeadline(new Date(0));
lockRecord.setStatus(1);
lockMapper.insertRecord(lockRecord);
}
return lockRecord;
}
/**
* 获取锁,代码片段
*/
@Override
@Transactional
public GetLockResponse getLock(GetLockRequest request) {
// 检测参数
if(StringUtils.isEmpty(request.lockName)) {
ResultUtil.throwBusinessException(CreditCardErrorCode.PARAM_INVALID);
}
// 兼容参数初始化
request.expireTime = null==request.expireTime? 31536000: request.expireTime;
request.desc = Strings.isNullOrEmpty(request.desc)?"":request.desc;
Long nowTime = new Date().getTime();
GetLockResponse response = new GetLockResponse();
response.lock = 0;
// 获取中央锁,初始化记录
lockMapper.queryRecordForUpdate("center_lock");
LockRecord lockRecord = initLockRecord(request.lockName);
// 未释放锁或未过期,获取失败
if (lockRecord.getStatus() == 1
&& lockRecord.getDeadline().getTime() > nowTime){
return response;
}
// 获取锁
Date deadline = new Date(nowTime + request.expireTime*1000);
int num = lockMapper.updateRecord(request.lockName, deadline, 0, request.desc, 1);
response.lock = 1;
return response;
}
На данный момент это решение может удовлетворить потребности моей распределенной блокировки.
Однако у этой схемы есть фатальная проблема: все записи имеют общую блокировку, а уровень параллелизма невысок.
После тестирования откройте 50 * 100 потоков для одновременного изменения, и это займет в среднем 8 секунд для 5 раз.
Реализовать третье издание
Из-за второго решения есть запросы, которые используют один и тот же центральный замок и имеют низкий параллелизм. Ссылаясь на принцип реализации concurrentHashMap, концепция сегментированной блокировки введена для уменьшения детализации блокировки.
Основной способ:
- Создать базу данных на основе sql
- Создайте 100 записей с Flock_name="center_lock_xx" (xx — это 00-99).
- При работе с другими замками (например, Flock_name="sale_invite_lock") найдите соответствующий center_lock_02 в соответствии с алгоритмом crc32 и сначала выберите для обновления запись "center_lock_02".
- «sale_invite_lock» записывает собственные добавления, удаления и изменения.
Псевдокод выглядит следующим образом:
// 开启事务
@Transactional
public boolean getLock(String key){
// 获取中央锁
select * from tbl where Flock_name="center_lock"
// 查询key相关记录
select for update
if (记录存在){
update
}else {
insert
}
}
/**
* 获取中央锁Key
*/
private boolean getCenterLock(String key){
String prefix = "center_lock_";
Long hash = SecurityUtil.crc32(key);
if (null == hash){
return false;
}
//取crc32中的最后两位值
Integer len = hash.toString().length();
String slot = hash.toString().substring(len-2);
String centerLockKey = prefix + slot;
lockMapper.queryRecordForUpdate(centerLockKey);
return true;
}
/**
* 获取锁
*/
@Override
@Transactional
public GetLockResponse getLock(GetLockRequest request) {
// 检测参数
if(StringUtils.isEmpty(request.lockName)) {
ResultUtil.throwBusinessException(CreditCardErrorCode.PARAM_INVALID);
}
// 兼容参数初始化
request.expireTime = null==request.expireTime? 31536000: request.expireTime;
request.desc = Strings.isNullOrEmpty(request.desc)?"":request.desc;
Long nowTime = new Date().getTime();
GetLockResponse response = new GetLockResponse();
response.lock = 0;
// 获取中央锁,初始化记录
getCenterLock(request.lockName);
LockRecord lockRecord = initLockRecord(request.lockName);
// 未释放锁或未过期,获取失败
if (lockRecord.getStatus() == 1
&& lockRecord.getDeadline().getTime() > nowTime){
return response;
}
// 获取锁
Date deadline = new Date(nowTime + request.expireTime*1000);
int num = lockMapper.updateRecord(request.lockName, deadline, 0, request.desc, 1);
response.lock = 1;
return response;
}
После тестирования 50*100 потоков включаются для одновременной модификации, а среднее время для 5 раз составляет 5 секунд. По сравнению с версией 2 это улучшение почти в два раза.
На данный момент реализация и применение распределенных блокировок и счетчиков redis/mysql завершены.
Наконец
В соответствии с различными сценариями применения выберите следующие варианты:
- Высокий параллелизм, отсутствие гарантии согласованности данных: блокировка/счетчик Redis
- Низкий уровень параллелизма, гарантированная согласованность данных: mysql lock/counter
- Низкий уровень параллелизма, отсутствие гарантии согласованности данных: вы можете делать все, что хотите
- Высокий параллелизм. Обеспечьте согласованность данных: блокировка/счетчик redis + блокировка/счетчик mysql.
Табличные данные и записи:
Добро пожаловать, чтобы следить за моим коротким книжным блогом, расти и прогрессировать вместе.