详解Redis分布式锁(图文并茂,手把手搭建服务,代码详解,建议收藏)(1)

jedisPoolConfig.setMaxTotal(maxTotal);

jedisPoolConfig.setMaxIdle(maxIdle);

jedisPoolConfig.setMinIdle(minIdle);

JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);

logger.info(“JedisPool注入成功!!”);

logger.info(“redis地址:” + host + “:” + port);

return jedisPool;

}

}

application.yml配置文件

server:

port: 18080

spring:

redis:

database: 0

host: 127.0.0.1

port: 6379

timeout: 10000

password:

jedis:

pool:

max-active: 20

max-idle: 20

min-idle: 0

获取锁与释放锁代码

package com.lizba.utill;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.params.SetParams;

import java.util.Arrays;

import java.util.concurrent.TimeUnit;

/**

  •   Redis分布式锁简单工具类
    
  • @Author: Liziba

  • @Date: 2021/7/11 11:42

*/

@Service

public class RedisLockUtil {

private static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

/**

  • 锁键 -> key

*/

private final String LOCK_KEY = “lock_key”;

/**

  • 锁过期时间 -> TTL

*/

private Long millisecondsToExpire = 10000L;

/**

  • 获取锁超时时间 -> get lock timeout for return

*/

private Long timeout = 300L;

/**

  • LUA脚本 -> 分布式锁解锁原子操作脚本

*/

private static final String LUA_SCRIPT =

“if redis.call(‘get’,KEYS[1]) == ARGV[1] then” +

" return redis.call(‘del’,KEYS[1]) " +

“else” +

" return 0 " +

“end”;

/**

  • set命令参数

*/

private SetParams params = SetParams.setParams().nx().px(millisecondsToExpire);

@Autowired

private JedisPool jedisPool;

/**

  • 加锁 -> 超时锁

  • @param lockId 一个随机的不重复id -> 区分不同客户端

  • @return

*/

public boolean timeLock(String lockId) {

Jedis client = jedisPool.getResource();

long start = System.currentTimeMillis();

try {

for(;😉 {

String lock = client.set(LOCK_KEY, lockId, params);

if (“OK”.equalsIgnoreCase(lock)) {

return Boolean.TRUE;

}

// sleep -> 获取失败暂时让出CPU资源

TimeUnit.MILLISECONDS.sleep(100);

long time = System.currentTimeMillis() - start;

if (time >= timeout) {

return Boolean.FALSE;

}

}

} catch (Exception e) {

e.printStackTrace();

logger.error(e.getMessage());

} finally {

client.close();

}

return Boolean.FALSE;

}

/**

  • 解锁

  • @param lockId 一个随机的不重复id -> 区分不同客户端

  • @return

*/

public boolean unlock(String lockId) {

Jedis client = jedisPool.getResource();

try {

Object result = client.eval(LUA_SCRIPT, Arrays.asList(LOCK_KEY), Arrays.asList(lockId));

if (result != null && “1”.equalsIgnoreCase(result.toString())) {

return Boolean.TRUE;

}

return Boolean.FALSE;

} catch (Exception e) {

e.printStackTrace();

logger.error(e.getMessage());

}

return Boolean.FALSE;

}

}

测试类

package com.lizba.controller;

import cn.hutool.core.util.IdUtil;

import com.lizba.utill.RedisLockUtil;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import java.util.HashSet;

import java.util.Set;

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.atomic.AtomicInteger;

/**

  •   测试
    
  • @Author: Liziba

  • @Date: 2021/7/11 12:27

*/

@RestController

@RequestMapping(“/redis”)

public class TestController {

@Autowired

private RedisLockUtil redisLockUtil;

private AtomicInteger count ;

@GetMapping(“/index/{num}”)

public String index(@PathVariable int num) throws InterruptedException {

count = new AtomicInteger(0);

CountDownLatch countDownLatch = new CountDownLatch(num);

ExecutorService executorService = Executors.newFixedThreadPool(num);

Set failSet = new HashSet<>();

long start = System.currentTimeMillis();

for (int i = 0; i < num; i++) {

executorService.execute(() -> {

long lockId = IdUtil.getSnowflake(1, 1).nextId();

try {

boolean isSuccess = redisLockUtil.timeLock(String.valueOf(lockId));

if (isSuccess) {

count.addAndGet(1);

System.out.println(Thread.currentThread().getName() + " lock success" );

} else {

failSet.add(Thread.currentThread().getName());

}

} finally {

boolean unlock = redisLockUtil.unlock(String.valueOf(lockId));

if (unlock) {

System.out.println(Thread.currentThread().getName() + " unlock success" );

}

}

countDownLatch.countDown();

});

}

countDownLatch.await();

executorService.shutdownNow();

failSet.forEach(t -> System.out.println(t + " lock fail" ));

long time = System.currentTimeMillis() - start;

return String.format(“Thread sum: %d, Time sum: %d, Success sum:%d”, num, time, count.get());

}

}

测试结果

image.png

image.png

2.3 Redis的超时问题

Redis分布式锁有一个问题是锁的超时问题,也就是说如果客户端A获取到锁之后去执行任务,任务没跑完锁的超时时间到了,锁就会自动释放,这个时候客户端B就能乘虚而入了,锁就会出现问题!

关于这个问题其实并没有完全的解决办法,但是能通过如下手段去优化:

  1. 尽可能不要在Redis分布式锁中执行较长的任务,尽可能的缩小锁区间内执行代码,就像单JVM锁中的synchronized优化一样,我们可以考虑优化锁的区间

  2. 多做压力测试和线上真实场景的模拟测试,估算一个合适的锁超时时间

  3. 做好Redis分布式锁超时任务未执行完的问题发生后,数据恢复手段的准备

三、集群中的分布式锁


3.1 集群分布式锁存在的问题

上述的分布式锁,针对单节点实例的Redis是可行的;但是我们在公司根本不会用单节点的Redis实例,往往采用最简单的都是是Redis一主二从+Sentinel监控配置;在sentinel集群中,虽然主节点挂掉时,从节点会取而代之,客户端无感知,但是上述的分布式锁就可能存在节点之间数据同步异常导致分布式锁失效的问题。

正常情况下客户端向sentinel监控的Redis集群申请分布式锁:

Redis哨兵模式申请锁.png

比如,客户端A在主节点(机器1)上申请了一把锁,此时主节点(机器1)挂掉了且锁没来得及同步到从节点(机器2和机器3),此时从节点(机器2)成为了新的主节点,但是锁在新的主节点(机器2)上并不存在,所以客户端B申请锁成功,锁的定义在这种场景中就出现了问题!

主节点宕机锁同步失败情况,其他客户端申请锁成功:

Redis哨兵模式master宕机申请锁.png

上述这种情况虽然之后发生在主从发生failover的情况才产生,但显然是不安全的,普通的业务系统或许能接受,但大金额的业务场景是不允许出现的。

3.2 RedLock

3.2.1 简介

解决这个问题的办法就是使用RedLock算法,也称红锁。RedLock通过使用多个Redis实例,各个实例之间没有主从关系,相互独立;加锁的时候,客户端向所有的节点发送加锁指令,如果过半的节点set成功,就加锁成功。释放锁时,需要向所有的节点发送del指令来释放锁。RedLock的实现思路比较简单,但是实际算法比较复杂,需要考虑非常多的细节问题,如出错重试,时钟漂移等。此外RedLock需要新增较多的实例来申请分布式锁,不仅消耗服务器资源,也会有一定的性能下降。

其架构图如下,客户端向多个独立的Redis服务发送加锁指令(为了追求高吞吐量和低延时,客户端需要使用多路传输来对N个Redis Server服务器进行通信),过半反馈成功则加锁成功,Redis Server的个数最后为奇数。

Redis 中文版网站介绍

REDIS distlock – Redis中国用户组(CRUG)

在上述的架构图中,存在5台Redis服务器用于获取锁,那么此时一个客户端获取锁需要做哪些操作呢?

  1. 获取系统当前时间(ms)

  2. 使用相同的key和随机值在5个节点上请求锁,请求锁的过程中包含多个时间值的定义,包括请求单个锁超时时间,请求锁的总耗时时间,锁自动释放时间。单个锁请求的超时时间不宜过大,防止请求宕机的Redis服务阻塞时间过长。

  3. 客户端计算获取锁的总时长和获取锁成功的个数,当所得个数大于等于3且获取锁的时间小于锁的自动释放时间才算成功

  4. 锁获取成功,则锁自动释放时间等于TTL减去获取所得消耗的时间(这个锁消耗的时间计算比较复杂)

  5. 锁获取失败,向所有的Redis服务器发送删除指令,一定是所有的Redis服务器都要发送

  6. 失败重试,锁获取失败后进行重试,重试的时间应该是一个随机值,避免与其他客户端同时请求锁而加大失败的可能,且这个时间应该大于获取锁消耗的时间

3.2.2 锁的最小有效时长

由于上面说到存在时钟漂移的问题,并且客户端向不同的Redis服务器请求锁的时间也会有细微的差异,所以有必要认真的研究一下客户端获取到的锁的最小有效时长计算:

假设客户端申请锁成功,第一个key设置成功的时间为TF,最后一个key设置成功的时间为TL,锁的超时时间为TTL,不同进程之间的时钟差异为CLOCK_DIFF,则锁的最小有效时长是:

TIME = TTL - (TF- TL) - CLOCK_DIFF

RedLock锁的有效时长.png

3.2.3 故障恢复

采用Redis来实现分布式锁,离不开服务器宕机等不可用问题,这里RedLock红锁也一样,即使是多台服务器申请锁,我们也要考虑服务器宕机后的处理,官方建议采用AOF持久化处理。

但是AOF持久化只对正常SHUTDOWN这种指令能做到重启恢复,但是如果是断电的情况,可能导致最后一次持久化到断电期间的锁数据丢失,当服务器重启后,可能会出现分布式锁语义错误的情况。所以为了规避这种情况,官方建议Redis服务重启后,一个最大客户端TTL时间内该Redis服务不可用(不提供申请锁的服务),这确实可以解决问题,但是显而易见这肯定影响Redis服务器的性能,并且在多数节点都出现这种情况的时候,系统将出现全局不可用的状态。

3.3 Redisson实现分布式锁

3.3.1 Redission简介

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

Redission github地址:

https://github.com/redisson/redisson

Redission 分布式锁和同步器Wiki:

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

总而言之——Redisson非常强大

3.3.2 Reddison之RedLock使用

pom依赖

org.redisson

redisson

3.3.2

测试类

package com.liziba.util;

import org.redisson.Redisson;

import org.redisson.RedissonRedLock;

import org.redisson.api.RLock;

import org.redisson.api.RedissonClient;

import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

/**

  •  测试Redisson 之 RedLock
    
  • @Author: Liziba

  • @Date: 2021/7/11 20:55

*/

public class LockTest {

private static final String resourceName = “REDLOCK_KEY”;

private static RedissonClient cli_79;

private static RedissonClient cli_89;

private static RedissonClient cli_99;

static {

Config config_79 = new Config();

config_79.useSingleServer()

.setAddress(“127.0.0.1:6379”) // 注意这里我的Redis测试实例没密码

.setDatabase(0);

cli_79 = Redisson.create(config_79);

Config config_89 = new Config();

config_89.useSingleServer()

.setAddress(“127.0.0.1:6389”)

.setDatabase(0);

cli_89 = Redisson.create(config_89);

Config config_99 = new Config();

config_99.useSingleServer()

.setAddress(“127.0.0.1:6399”)

.setDatabase(0);

cli_99 = Redisson.create(config_99);

}

/**

  • 加锁操作

*/

private static void lock () {

// 向3个Redis实例尝试加锁

RLock lock_79 = cli_79.getLock(resourceName);

RLock lock_89 = cli_89.getLock(resourceName);

RLock lock_99 = cli_99.getLock(resourceName);

RedissonRedLock redLock = new RedissonRedLock(lock_79, lock_89, lock_99);

try {

boolean isLock = redLock.tryLock(100, 10000, TimeUnit.MILLISECONDS);

if (isLock) {

// do something …

System.out.println(Thread.currentThread().getName() + “Get Lock Success!”);

TimeUnit.MILLISECONDS.sleep(10000);

} else {

System.out.println(Thread.currentThread().getName() + “Get Lock fail!”);

}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

// 无论如何一定要释放锁 -> 这里会像所有的Redis服务释放锁

redLock.unlock();

}

}

public static void main(String[] args) {

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

image

上述的面试题答案都整理成文档笔记。 也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)

image

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

ck_99);

try {

boolean isLock = redLock.tryLock(100, 10000, TimeUnit.MILLISECONDS);

if (isLock) {

// do something …

System.out.println(Thread.currentThread().getName() + “Get Lock Success!”);

TimeUnit.MILLISECONDS.sleep(10000);

} else {

System.out.println(Thread.currentThread().getName() + “Get Lock fail!”);

}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

// 无论如何一定要释放锁 -> 这里会像所有的Redis服务释放锁

redLock.unlock();

}

}

public static void main(String[] args) {

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

[外链图片转存中…(img-xqNZmcF9-1714800429590)]

上述的面试题答案都整理成文档笔记。 也还整理了一些面试资料&最新2021收集的一些大厂的面试真题(都整理成文档,小部分截图)

[外链图片转存中…(img-u1OWTadh-1714800429590)]

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 22
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值