1. Introdução
Recentemente, a frequência do projeto on-line é bastante alta.Depois de trabalhar horas extras por vários dias, o corpo fica um pouco sobrecarregado e o espírito fica um pouco fraco, mas o lado comercial está desesperadamente pressionado. Quando o cérebro está confuso, você não pode chamá-lo de código, pode chamá-lo diretamente Bug
. Fiquei acordada a noite toda e escrevi uma que bug
foi repreendida.
Porque o shopping é para fazer negócios, a freqüentar as deduções estoque de mercadorias, Application Clusters é implantado, a fim de evitar o inventário causa concorrente 超买超卖
e outras questões, o uso de redis
bloqueio distribuído para ser controlado. Eu pensei que lock.tryLock
seria bom adicionar um bloqueio ao código de inventário deduzido
/**
* @author xiaofu
* @description 扣减库存
* @date 2020/4/21 12:10
*/
public String stockLock() {
RLock lock = redissonClient.getLock("stockLock");
try {
/**
* 获取锁
*/
if (lock.tryLock(10, TimeUnit.SECONDS)) {
/**
* 查询库存数
*/
Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stockCount"));
/**
* 扣减库存
*/
if (stock > 0) {
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stockCount", stock.toString());
LOGGER.info("库存扣减成功,剩余库存数量:{}", stock);
} else {
LOGGER.info("库存不足~");
}
} else {
LOGGER.info("未获取到锁业务结束..");
}
} catch (Exception e) {
LOGGER.info("处理异常", e);
} finally {
lock.unlock();
}
return "ok";
}
Como resultado, após a execução do código comercial, eu esqueci de liberar o bloqueio lock.unlock()
, o que causou a redis
cheia do pool de threads, uma redis
falha em larga escala do serviço e a confusão da dedução dos dados do inventário. O líder foi repreendido.
Com o uso do redis
tempo de bloqueio mais longo, eu encontrei o redis
bloqueio do poço a ser mais longe do que o esperado. Mesmo nas perguntas da entrevista, redis
a taxa de aparência de bloqueios distribuídos é relativamente alta, como: "Quais problemas você encontrou com bloqueios?" E "Como resolvê-los?" Basicamente, todos eles são solicitados por um conjunto de traços.
Hoje, compartilharei meu redis
diário com um cadeado distribuído e algumas soluções, e encorajo você.
1. O bloqueio não é liberado
Este caso é um erro estúpido que eu cometi o erro no topo, devido à thread atual para adquirir redis
o bloqueio, o bloqueio não é liberado em tempo após operações de processamento, resultando em outros segmentos vão sempre tentar adquirir o bloqueio de bloqueio, por exemplo: O Jedis
cliente será relatado como segue Mensagem de erro
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
redis线程池
Não há threads ociosos para processar comandos do cliente.
A solução também é muito simples: desde que tenhamos cuidado, o thread que obteve o bloqueio liberará o bloqueio a tempo após o processamento dos negócios.Se o bloqueio não for reinserido, o thread poderá liberar a conexão atual sleep
por um período de tempo.
public void lock() {
while (true) {
boolean flag = this.getLock(key);
if (flag) {
TODO .........
} else {
// 释放当前redis连接
redis.close();
// 休眠1000毫秒
sleep(1000);
}
}
}
Segundo, a trava de B foi liberada por A
Sabemos que Redis
o princípio é atingir um bloqueio SETNX
de comando. Quando key
a ausência do key
valor é definido value
, o valor de retorno 1
; se dado key
já existe, SETNX
sem qualquer ação, o valor de retorno 0
.
SETNX key value
Vamos imaginar esta cena: A
, B
dois segmentos tentam key
myLock
bloqueio, A线程
para obter um bloqueio (se o bloqueio 3秒
expira), B线程
apenas esperando para tentar adquirir o bloqueio, este ponto não é errado.
Se neste momento a lógica de negócios que é demorado, o tempo de execução ultrapassou o redis
bloqueio expirado o tempo, quando A线程
o bloqueio é automaticamente liberada (suprimido key
), B线程
detecta myLock
isso key
não existe, SETNX
o comando também tem um bloqueio.
No entanto, A线程
depois de executar a lógica de negócios no momento , o bloqueio ainda será liberado (excluído key
), o que faz com que B线程
o bloqueio seja A线程
liberado.
Para evitar a situação acima, geralmente precisamos trazer nosso próprio value
valor exclusivo para identificar quando cada encadeamento está bloqueado e liberar apenas o value
valor especificado key
; caso contrário, haverá uma bagunça ao liberar o bloqueio.
Três, tempo limite da transação do banco de dados
emm ~ Os redis
bloqueios de bate-papo ainda estão envolvidos nas transações do banco de dados? Não se apresse em olhar para baixo, observe o seguinte código:
@Transaction
public void lock() {
while (true) {
boolean flag = this.getLock(key);
if (flag) {
insert();
}
}
}
Adicione uma @Transaction
observação a este método para iniciar a transação, como lançar uma exceção no código para reverter, para saber que a transação do banco de dados tem um limite de tempo limite e não aguardará incondicionalmente uma operação de banco de dados demorada.
Por exemplo: analisamos um arquivo grande e armazenamos os dados no banco de dados.Se o tempo de execução for muito longo, fará com que a transação seja revertida automaticamente quando atingir o tempo limite.
Depois que você key
não adquirir o bloqueio por um longo tempo, e o bloqueio de aquisição estiver 等待的时间
muito além da transação do banco de dados 超时时间
, o programa reportará uma exceção.
Geralmente, para resolver esse problema, precisamos alterar a transação do banco de dados para confirmar e reverter manualmente a transação.
@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
@Transaction
public void lock() {
//手动开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
while (true) {
boolean flag = this.getLock(key);
if (flag) {
insert();
//手动提交事务
dataSourceTransactionManager.commit(transactionStatus);
}
}
} catch (Exception e) {
//手动回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
}
}
4. O bloqueio expirou e o negócio não foi executado
Essa situação é semelhante à segunda mencionada acima, mas a solução é um pouco diferente.
É o mesmo caso em que o redis
bloqueio distribuído expira e a lógica de negócios não é concluída, mas há outra maneira de pensar sobre o problema: não seria resolvido se o redis
tempo de expiração do bloqueio fosse maior?
Ainda existe um problema. Podemos redis
aumentar manualmente o tempo de expiração do bloqueio quando o bloqueamos. Mas por quanto tempo esse tempo é adequado? O tempo de execução da lógica de negócios é incontrolável e, se for ajustado por muito tempo, afetará o desempenho da operação.
Seria bom se o redis
tempo de expiração do bloqueio pudesse ser renovado automaticamente.
Para solucionar esse problema, usamos o redis
cliente redisson
, redisson
que resolve redis
alguns problemas difíceis em um ambiente distribuído , cujo objetivo é permitir que os usuários reduzam Redis
sua atenção e gastem mais energia no processamento da lógica de negócios.
redisson
O bloqueio distribuído é bem encapsulado, basta chamá-lo API
.
RLock lock = redissonClient.getLock("stockLock");
redisson
Após o bloqueio ser adicionado com êxito, uma tarefa agendada será registrada para monitorar o bloqueio, e o bloqueio será verificado a cada 10 segundos.Se o bloqueio ainda estiver em espera, o bloqueio será 过期时间
renovado. O tempo de expiração padrão é 30 segundos. Este mecanismo também é chamado: " 看门狗
", o nome. . .
Exemplo : se o tempo de bloqueio for 30 segundos, verifique uma vez após 10 segundos, depois que o serviço de bloqueio não for executado, ele será renovado e o tempo de expiração do bloqueio será redefinido para 30 segundos novamente.
Pela análise a seguir redisson
ele pode ser encontrado no código fonte para conseguir, se é 加锁
, 解锁
, 续约
é o cliente de alguma lógica de negócios complexos, encapsulando o Lua
roteiro enviado para redis
assegurar a implementação desta lógica de negócios complexos 原子性
.
@Slf4j
@Service
public class RedisDistributionLockPlus {
/**
* 加锁超时时间,单位毫秒, 即:加锁时间内执行完操作,如果未完成会有并发现象
*/
private static final long DEFAULT_LOCK_TIMEOUT = 30;
private static final long TIME_SECONDS_FIVE = 5 ;
/**
* 每个key的过期时间 {@link LockContent}
*/
private Map<String, LockContent> lockContentMap = new ConcurrentHashMap<>(512);
/**
* redis执行成功的返回
*/
private static final Long EXEC_SUCCESS = 1L;
/**
* 获取锁lua脚本, k1:获锁key, k2:续约耗时key, arg1:requestId,arg2:超时时间
*/
private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[2]) == 1 then ARGV[2] = math.floor(redis.call('get', KEYS[2]) + 10) end " +
"if redis.call('exists', KEYS[1]) == 0 then " +
"local t = redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2]) " +
"for k, v in pairs(t) do " +
"if v == 'OK' then return tonumber(ARGV[2]) end " +
"end " +
"return 0 end";
/**
* 释放锁lua脚本, k1:获锁key, k2:续约耗时key, arg1:requestId,arg2:业务耗时 arg3: 业务开始设置的timeout
*/
private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"local ctime = tonumber(ARGV[2]) " +
"local biz_timeout = tonumber(ARGV[3]) " +
"if ctime > 0 then " +
"if redis.call('exists', KEYS[2]) == 1 then " +
"local avg_time = redis.call('get', KEYS[2]) " +
"avg_time = (tonumber(avg_time) * 8 + ctime * 2)/10 " +
"if avg_time >= biz_timeout - 5 then redis.call('set', KEYS[2], avg_time, 'EX', 24*60*60) " +
"else redis.call('del', KEYS[2]) end " +
"elseif ctime > biz_timeout -5 then redis.call('set', KEYS[2], ARGV[2], 'EX', 24*60*60) end " +
"end " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
/**
* 续约lua脚本
*/
private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
private final StringRedisTemplate redisTemplate;
public RedisDistributionLockPlus(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
ScheduleTask task = new ScheduleTask(this, lockContentMap);
// 启动定时任务
ScheduleExecutor.schedule(task, 1, 1, TimeUnit.SECONDS);
}
/**
* 加锁
* 取到锁加锁,取不到锁一直等待知道获得锁
*
* @param lockKey
* @param requestId 全局唯一
* @param expire 锁过期时间, 单位秒
* @return
*/
public boolean lock(String lockKey, String requestId, long expire) {
log.info("开始执行加锁, lockKey ={}, requestId={}", lockKey, requestId);
for (; ; ) {
// 判断是否已经有线程持有锁,减少redis的压力
LockContent lockContentOld = lockContentMap.get(lockKey);
boolean unLocked = null == lockContentOld;
// 如果没有被锁,就获取锁
if (unLocked) {
long startTime = System.currentTimeMillis();
// 计算超时时间
long bizExpire = expire == 0L ? DEFAULT_LOCK_TIMEOUT : expire;
String lockKeyRenew = lockKey + "_renew";
RedisScript<Long> script = RedisScript.of(LOCK_SCRIPT, Long.class);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
keys.add(lockKeyRenew);
Long lockExpire = redisTemplate.execute(script, keys, requestId, Long.toString(bizExpire));
if (null != lockExpire && lockExpire > 0) {
// 将锁放入map
LockContent lockContent = new LockContent();
lockContent.setStartTime(startTime);
lockContent.setLockExpire(lockExpire);
lockContent.setExpireTime(startTime + lockExpire * 1000);
lockContent.setRequestId(requestId);
lockContent.setThread(Thread.currentThread());
lockContent.setBizExpire(bizExpire);
lockContent.setLockCount(1);
lockContentMap.put(lockKey, lockContent);
log.info("加锁成功, lockKey ={}, requestId={}", lockKey, requestId);
return true;
}
}
// 重复获取锁,在线程池中由于线程复用,线程相等并不能确定是该线程的锁
if (Thread.currentThread() == lockContentOld.getThread()
&& requestId.equals(lockContentOld.getRequestId())){
// 计数 +1
lockContentOld.setLockCount(lockContentOld.getLockCount()+1);
return true;
}
// 如果被锁或获取锁失败,则等待100毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
// 这里用lombok 有问题
log.error("获取redis 锁失败, lockKey ={}, requestId={}", lockKey, requestId, e);
return false;
}
}
}
/**
* 解锁
*
* @param lockKey
* @param lockValue
*/
public boolean unlock(String lockKey, String lockValue) {
String lockKeyRenew = lockKey + "_renew";
LockContent lockContent = lockContentMap.get(lockKey);
long consumeTime;
if (null == lockContent) {
consumeTime = 0L;
} else if (lockValue.equals(lockContent.getRequestId())) {
int lockCount = lockContent.getLockCount();
// 每次释放锁, 计数 -1,减到0时删除redis上的key
if (--lockCount > 0) {
lockContent.setLockCount(lockCount);
return false;
}
consumeTime = (System.currentTimeMillis() - lockContent.getStartTime()) / 1000;
} else {
log.info("释放锁失败,不是自己的锁。");
return false;
}
// 删除已完成key,先删除本地缓存,减少redis压力, 分布式锁,只有一个,所以这里不加锁
lockContentMap.remove(lockKey);
RedisScript<Long> script = RedisScript.of(UNLOCK_SCRIPT, Long.class);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
keys.add(lockKeyRenew);
Long result = redisTemplate.execute(script, keys, lockValue, Long.toString(consumeTime),
Long.toString(lockContent.getBizExpire()));
return EXEC_SUCCESS.equals(result);
}
/**
* 续约
*
* @param lockKey
* @param lockContent
* @return true:续约成功,false:续约失败(1、续约期间执行完成,锁被释放 2、不是自己的锁,3、续约期间锁过期了(未解决))
*/
public boolean renew(String lockKey, LockContent lockContent) {
// 检测执行业务线程的状态
Thread.State state = lockContent.getThread().getState();
if (Thread.State.TERMINATED == state) {
log.info("执行业务的线程已终止,不再续约 lockKey ={}, lockContent={}", lockKey, lockContent);
return false;
}
String requestId = lockContent.getRequestId();
long timeOut = (lockContent.getExpireTime() - lockContent.getStartTime()) / 1000;
RedisScript<Long> script = RedisScript.of(RENEW_SCRIPT, Long.class);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
Long result = redisTemplate.execute(script, keys, requestId, Long.toString(timeOut));
log.info("续约结果,True成功,False失败 lockKey ={}, result={}", lockKey, EXEC_SUCCESS.equals(result));
return EXEC_SUCCESS.equals(result);
}
static class ScheduleExecutor {
public static void schedule(ScheduleTask task, long initialDelay, long period, TimeUnit unit) {
long delay = unit.toMillis(initialDelay);
long period_ = unit.toMillis(period);
// 定时执行
new Timer("Lock-Renew-Task").schedule(task, delay, period_);
}
}
static class ScheduleTask extends TimerTask {
private final RedisDistributionLockPlus redisDistributionLock;
private final Map<String, LockContent> lockContentMap;
public ScheduleTask(RedisDistributionLockPlus redisDistributionLock, Map<String, LockContent> lockContentMap) {
this.redisDistributionLock = redisDistributionLock;
this.lockContentMap = lockContentMap;
}
@Override
public void run() {
if (lockContentMap.isEmpty()) {
return;
}
Set<Map.Entry<String, LockContent>> entries = lockContentMap.entrySet();
for (Map.Entry<String, LockContent> entry : entries) {
String lockKey = entry.getKey();
LockContent lockContent = entry.getValue();
long expireTime = lockContent.getExpireTime();
// 减少线程池中任务数量
if ((expireTime - System.currentTimeMillis())/ 1000 < TIME_SECONDS_FIVE) {
//线程池异步续约
ThreadPool.submit(() -> {
boolean renew = redisDistributionLock.renew(lockKey, lockContent);
if (renew) {
long expireTimeNew = lockContent.getStartTime() + (expireTime - lockContent.getStartTime()) * 2 - TIME_SECONDS_FIVE * 1000;
lockContent.setExpireTime(expireTimeNew);
} else {
// 续约失败,说明已经执行完 OR redis 出现问题
lockContentMap.remove(lockKey);
}
});
}
}
}
}
}
Cinco, redis poço de replicação mestre-escravo
redis
A solução mais comum para alta disponibilidade é 主从复制
(mestre-escravo), que também redis分布式锁
abre um buraco.
redis cluster
Em um ambiente em cluster, se você A客户端
deseja bloquear agora , ele selecionará um master
nó para gravar de acordo com as regras de roteamento.Depois que key
mylock
o bloqueio for bem-sucedido, o master
nó será key
copiado de forma assíncrona no slave
nó correspondente .
Se o redis master
nó estiver inativo no momento , para garantir a disponibilidade do cluster, ele continuará 主备切换
e slave
se tornará redis master
. B客户端
O master
bloqueio A客户端
foi adicionado com sucesso no novo nó e eu pensei que tinha adicionado com sucesso.
Nesse momento, ele fará com que vários clientes concluam o bloqueio em um bloqueio distribuído ao mesmo tempo, resultando na geração de vários dados sujos.
Quanto à solução, atualmente, não há cura, apenas pode garantir a estabilidade da máquina e reduzir a probabilidade desse evento.
Sumário
A seguir estão Redis
alguns dos poços que encontrei ao usar bloqueios distribuídos. Fiquei um pouco lamentado. Muitas vezes preenchi esse poço com um método. Não demorou muito para encontrar outro poço. O que é uma bala de prata, depois de ponderar os prós e os contras, escolhe um compromisso dentro do intervalo aceito.
Pequenos benefícios:
Existem alguns cursos pagos, gratuitos , para amigos. Preste atenção na minha conta pública [algo dentro do programador], responda [ 666 ]