Redis7-分布式锁

目录

基本原理

分布式锁的实现

基于Redis的分布式锁

Redis分布式锁误删

分布式锁的原子性问题

基于Redis的分布式锁优化

Redission概述

Redisson入门

Redisson可重入锁原理

​编辑

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

Redission的MultiLock原理

分布式锁总结


基本原理

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

分布式锁需要满足的条件:

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化
  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  • 高性能:由于加锁本身就让性能降低,对于分布式锁本身需要有较高的加锁性能和释放锁性能
  • 安全性

分布式锁的实现

分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:

基于Redis的分布式锁

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

1.获取锁

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

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

2.释放锁

  • 手动释放

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

流程:

代码实现:

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

java 复制代码
public class SimpleRedisLock implements ILock{
  private static final String KEY_PREFIX="lock:";

  @Override
  public boolean tryLock(long timeoutSec) {
      //获取线程标示
      String threadId = Thread.currentThread().getId();
      //获取锁
      Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
      return Boolean.TRUE.equals(success);
  }

  @Override
  public void unlock() {
      //通过del删除锁
      stringRedisTemplate.delete(KEY_PREFIX + name);
  }
}
java 复制代码
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象(新增代码)
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象
        boolean isLock = lock.tryLock(1200);
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

Redis分布式锁误删

持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

需求:

修改之前的分布式锁实现,满足:

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁

  • 如果不一致则不释放锁

代码实现:

获取锁

java 复制代码
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 + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

释放锁

java 复制代码
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);
    }
}

分布式锁的原子性问题

线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题

Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法:Lua 教程 | 菜鸟教程

Redis提供的调用函数:

java 复制代码
redis.call('命令名称', 'key', '其它参数', ...)

例:执行set name jack

java 复制代码
# 执行 set name jack
redis.call('set', 'name', 'jack')

例:先执行set name Rose,再执行get name

java 复制代码
# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

例:执行 redis.call('set', 'name', 'jack') 这个脚本

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

Lua脚本实现释放锁:

Lua 复制代码
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

利用Java代码调用Lua脚本改造分布式锁

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

public void unlock() {
    // 调用lua脚本
    stringRedisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(KEY_PREFIX + name),
            ID_PREFIX + Thread.currentThread().getId());
}

总结

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性

  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化

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

Redission概述

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现

Redission提供了分布式锁的多种多样的功能:

官网:https://redisson.org

Redisson入门

1.引入依赖:

XML 复制代码
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

2.配置Redisson客户端:

java 复制代码
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

3.使用Redission的分布式锁:

java 复制代码
@Resource
private RedissionClient redissonClient;

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

4.在 VoucherOrderServiceImpl注入RedissonClient:

java 复制代码
@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 使用分布式锁
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

Redisson可重入锁原理

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间

Redission的MultiLock原理

为了提高redis的可用性,会搭建集群或者主从,以主从为例

此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了

为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

MultiLock加锁原理:

当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试

分布式锁总结

1.不可重入Redis分布式锁

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入、无法重试、锁超时失效

2.可重入的Redis分布式锁

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
  • 缺陷:Redis宕机引起锁失效问题

3.Redisson的MultiLock

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂
相关推荐
水月梦镜花6 小时前
redis:list列表命令和内部编码
数据库·redis·list
掘金-我是哪吒7 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
ketil279 小时前
Ubuntu 安装 redis
redis
王佑辉11 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku06611 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
gorgor在码农12 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王12 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情12 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
minihuabei17 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng19 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust