Redis分布式锁

Redis分布式锁Redis分布式锁_第1张图片

Redis分布式锁_第2张图片
什么是分布式锁?

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
Redis分布式锁_第3张图片

分布式锁的实现

分布式锁的核心是实现多进程之间的互斥,而满足这一点的方式有很多,常见的有三种:

MySql Redis Zookeeper
互斥 利用mysql本身的互斥锁机制 利用setnx这样的互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间 临时节点,断开连接自动释放
基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

互斥:确保只有一个线程获取锁

非阻塞式:尝试一次,成功返回true,失败返回false

# 添加锁 利用setnx的互斥特性
    SETNX lock thread1
# 添加锁过期的时间,避免服务宕机引起的死锁
    EXPIRE lock 5
# 添加锁,NX是互斥,EX是设置超时时间 (使其具有了原子性)(最终命令)
    SET lock thread1 NX EX 10

在这里插入图片描述

  • 释放锁

手动释放:

超时释放:获取锁时添加一个超时时间

# 释放锁
    DEL key

在这里插入图片描述
大致流程:
Redis分布式锁_第4张图片
example:基于Redis实现分布式锁初级版本

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。
Redis分布式锁_第5张图片
类:

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec){
        // 获取线程标识
        long threadId = Thread.currentThread().getId();

        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX,threadId,timeoutSec, TimeUnit.SECONDS);


        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock(){
        // 释放锁
        stringRedisTemplate.delete(  KEY_PREFIX + name);
    }

}

并发问题1:
Redis分布式锁_第6张图片
为防止出现这种问题:获取锁标识判断是否一致

*** 改进Redis的分布式锁***:

需求:修改之前的分布式锁实现,满足︰
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁
  • 如果不一致则不释放锁
    example:
public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";


    @Override
    public boolean tryLock(long timeoutSec){
        // 获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();

        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX,threadId,timeoutSec, TimeUnit.SECONDS);


        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock(){
        // 获取线程标示
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        // 判断标识是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

}

Redis分布式锁_第7张图片
并发问题2:
Redis分布式锁_第8张图片
解决此问题,需要使判断锁标识和释放锁操作为一个原子性操作!

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
Lua是一种编程语言,它的基本语法大家可以参考网站: https://www.runoob.com/lua/lua-tutorial.html

这里重点介绍Redis提供的调用函数,语法如下:

# 执行redis命令
redis.call('命令名称','key','其它参数',...)

例如,我们要执行set name jack,则脚本是这样的:

# 执行set name jack
redis.call('set','name','jack')

例如,我们要先执行set name Rose,再执行get name,则脚本如下:

# 先执行 set name jack
redis.call('set','name','jack')
# 执行set name jack
local name = redis.call('set','name','jack')
# 返回
return name

写好脚本以后,需要使用Redis命令来调用脚本,调用脚本的常见命令如下(scription表示脚本的意思):
Redis分布式锁_第9张图片
例如,我们要执行redis.call(‘set’ , ‘name’ , ‘jack’)这个脚本,语法如下:
在这里插入图片描述
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入keys数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
在这里插入图片描述
Lua脚本语言的数组下标是从1开始的。

Redis的业务流程

  1. 获取锁中的线程标识
  2. 判断是否与指定的标示(当前线程标示)一致
  3. 如果一致则释放锁(删除)
  4. 如果不一致则什么都不做

如果用Lua脚本来表示是这样的:

-- 这里的 KEYS[1]  就是锁的key,这里的ARGV[1] 就是当前线程表示
-- 获取锁中的线程标示  get key
lcoal id = redis.call('get',KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del',KEYS[1])
end
return 0

需求:基于Lua脚本实现分布式锁的释放逻辑

提示:RedisTemplate调用Lua脚本的API如下:

Redis分布式锁_第10张图片
基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示·
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化

基于setnx实现的分布式锁存在下面的问题:

  1. 不可重入:同一个线程无法多次获取同一把锁

  2. 不可重试:获取锁只尝试一次就返回false,没有重试机制

  3. 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患

  4. 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
Redis分布式锁_第11张图片
官网地址:https://redisson.org

GitHub地址:https://github.com/redisson/redisson

Redisson入门
  1. 引入依赖:
	<dependency>	
        <groupid>org.redisson</groupid>
        <artifactId>redisson</artifactId>
        <version>3.13.6</version>
    <dependency>	
  1. 配置客户端
@Configuration
public class Redisconfig {
	@Bean
	public RedissonClient redissonclient() {
		//配置类
		Config config = new Config();
		//添加redis地址,这里添加了单点的地址,也可以使用config.useclusterServers ()添加集群
		config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassowrd("123321");
        //创建客户端
		return Redisson.create(config);
    }

  1. 使用Redisson的分布式锁
@Resource
private RedissonClient redissonclient;
 
@Test
void testRedisson() throws InterruptedException {
   	//获取锁(可重入),指定锁的名称
   	RLock lock = redissonclient.getLock( "anyLock");
	//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(110,TimeUnit.SECONDS);
	//判断释放获取成功
	if(isLock)
	try{
	system.out.println("执行业务");
    }
    finally{
	//释放锁
	lock.unlock();
        }
	}
}

Redisson可重入锁原理

Redis分布式锁_第12张图片
Redis分布式锁_第13张图片
Redisson分布式锁原理:
Redis分布式锁_第14张图片

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和Pubsub功能实现等待、唤醒、获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

总结

  1. 不可重入Redis分布式锁
  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入、无法重试、锁超时失效
  1. 可重入的Redis分布式锁:
  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续时间;利用信号量控制锁重试等待
  • 缺陷:redis宕机引起锁失效问题
  1. Redisson和multiLock
  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高、实现复杂

你可能感兴趣的:(Redis,redis,java,分布式)