【Redis|实战篇4】黑马点评|分布式锁

文章目录

4.分布式锁

4.1基本原理和不同实现方式对比

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

特点:

  • 多线程可见
  • 互斥
  • 高可用
  • 高性能
  • 安全性

分布式锁的常见实现方式:

4.2Redis的分布式锁实现思路

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

  • 获取锁

    • 互斥:确保只能有一个线程使用锁

      shell 复制代码
      # 添加锁
      set [key] [value] ex [time] nx
  • 释放锁

    • 手动释放

      shell 复制代码
      # 释放锁(除了使用del手动释放,还可超时释放)
      del [key]
    • 超时释放:获取锁时添加一个超时时间

4.3实现Redis的分布式锁

初级版本:定义一个类,实现接口,利用Redis实现分布式锁

java 复制代码
package com.hmdp.utils;

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间,过期自动释放
     * @return true表示获取成功
     */
    boolean tryLock(Long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

1.创建分布式锁

java 复制代码
public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;
    private String name;//锁的名称
    private static final String KEY_PREFIX = "lock:";

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程表示
        long id = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);
        //Boolean到boolean自动拆箱可能会有空指针问题,所以要判断一下
        return Boolean.TRUE.equals(success);
    }

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

2.使用分布式锁

删去之前的synchronized,使用基于Redis实现的分布式锁

java 复制代码
Long userId = UserHolder.getUser().getId();
        SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate,"order:" + userId);
        boolean isLock = lock.tryLock(5);
        if (!isLock) {
            //获取锁失败
            return Result.fail("一人只能下一单");
        }
        try {//获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }

锁:

  • KEY:标识要锁的资源
  • VALUE:标识锁的持有者(用于防误删)
4.4Redis分布式锁误删问题

此时还存在一个问题:线程1超时,锁释放,线程2上锁,线程2进行过程中,线程1结束将锁误删,使得线程3也能上锁

解决方法:释放锁时,获取锁标识并判断是否一致

锁标识:UUID+线程id

为啥不单独用线程id:因为线程id递增,集群中可能冲突

修改一下锁的实现:

标识加上UUID前缀

java 复制代码
private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
java 复制代码
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + ID_PREFIX + name, id + "", timeoutSec, TimeUnit.SECONDS);

修改释放锁的方法

java 复制代码
@Override
    public void unlock() {
        String currentThreadFlag = ID_PREFIX + Thread.currentThread().getId();
        String redisThreadFlag = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (currentThreadFlag != null && currentThreadFlag.equals(redisThreadFlag)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
4.5分布式锁的原子性问题

分布式锁的原子性问题:线程1判断锁成功,释放锁时阻塞,锁超时自动释放,线程2上锁,线程1阻塞结束将锁误删

解决方法:需确保判断和释放锁一起执行不能间隔(用Lua脚本实现)

Lua脚本中的Redis指令出错,会发生回滚以确保原子性

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

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Lua是一种编程语言,基本语法可参考Lua 教程 | 菜鸟教程

Redis提供的调用函数:

shell 复制代码
redis.call('命令名称','key','其它参数',...)
4.7Java调用lua脚本改造分布式锁

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

在java项目中使用Lua脚本调用redis:

  1. 把Lua脚本文件存入resource包
  2. 声明静态变量DefaultRedisScript<>用于存储Lua脚本
  3. 使用stringRedisTemplate.execute调用Lua脚本

在idea中编写Lua文件可以下载一个插件:EmmyLua

1.编写Lua文件

lua 复制代码
-- 比较缓存中的线程标识与当前线程标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
    -- 一致,直接删除
    return redis.call('del', KEYS[1])
end
-- 不一致,返回0
return 0

2.声明静态变量

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);//声明返回值类型
    }

3.修改释放锁的方法,调用Lua脚本

java 复制代码
@Override
    public void unlock() {
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
    }
4.8Redisson功能介绍

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

使用Redisson可以解决

Redisson是一个在Redis基础上实现的java驻内存数据网络。

它不仅提供了一系列的java常用对象,还提供了许多分布式服务,其中就包含了分布式锁的实现

官网:Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform

4.9Redisson快速入门
  1. 引入依赖:

    xml 复制代码
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.13.6</version>
            </dependency>
  2. 配置Redisson客户端:

    java 复制代码
    @Configuration
    public class RedissonConfig {
    
        @Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private String port;
        @Value("${spring.redis.password}")
        private String password;
    
        /**
         * 创建Redisson配置对象,然后交给IOC管理
         *
         * @return
         */
        @Bean
        public RedissonClient redissonClient() {
            // 获取Redisson配置对象
            Config config = new Config();
            // 添加redis地址,这里添加的是单节点地址,也可以通过 config.userClusterServers()添加集群地址
            config.useSingleServer().setAddress("redis://" + this.host + ":" + this.port)
                    .setPassword(this.password);
            // 获取RedisClient对象,并交给IOC进行管理
            return Redisson.create(config);
        }
    }
  3. 使用Redisson的分布式锁

    java 复制代码
    RLock lock = redissonClient.getLock("lock:order:" + userId);
            boolean isLock = lock.tryLock();//Redisson不用手动设置有效期
4.10Redisson的可重入锁原理

什么情况下会使用锁的可重入:

  1. 递归调用
  2. 方法链调用
  3. 回调函数或模版方法模式
  4. 避免死锁的防御性编程

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

4.11Redisson的锁重试和WatchDog机制

锁重试:加锁失败--->订阅--->唤醒线程--->加锁

WatchDog机制:给锁续期,防止锁失效了业务还没结束

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

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

缺陷:Redis宕机引起锁失效问题

4.12Redisson的multiLock原理

原理:多个独立的Redis节点,必须在所有节点都获取重入锁才算获取锁成功

缺陷:运维成本高,实现复杂

相关推荐
RuoyiOffice7 分钟前
企业请假销假系统设计实战:一张表、一套流程、两段生命周期——BPM节点驱动的表单变形术
java·spring·uni-app·vue·产品运营·ruoyi·anti-design-vue
鹤旗8 分钟前
While语句,do-while语句,for语句
java·jvm·算法
博语小屋12 分钟前
I/O 多路转接之epoll
运维·服务器·数据库
小碗羊肉18 分钟前
【从零开始学Java | 第十八篇】BigInteger
java·开发语言·新手入门
sky wide28 分钟前
[特殊字符] Docker Swarm 集群搭建指南
java·docker·容器
wuqingshun31415933 分钟前
谈谈你对springAop动态代理的理解?
java·jvm
执笔画流年呀35 分钟前
PriorityQueue(堆)续集
java·开发语言
问道飞鱼40 分钟前
【大模型学习】LangGraph 深度解析:定义、功能、原理与实践
数据库·学习·大模型·工作流
DJ斯特拉43 分钟前
黑马点评技术汇总(四)缓存雪崩 && 缓存击穿
数据库·缓存
武超杰44 分钟前
Spring Boot入门教程
java·spring boot·后端