【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节点,必须在所有节点都获取重入锁才算获取锁成功

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

相关推荐
沪漂阿龙在努力1 分钟前
别再被SQL的连表查询搞疯了!一文带你吃透Neo4j图数据库,从零搭建“关系网”
数据库
m0_748920363 分钟前
如何利用宝塔面板设置网站限流策略_防止恶意高并发请求
jvm·数据库·python
wsx_iot4 分钟前
arthas使用
java·arthas
正在走向自律4 分钟前
KingbaseES 基础 SQL 语法与日常运维实操手册
运维·数据库·sql·kingbasees
m0_7349497910 分钟前
C#怎么操作Redis缓存 C#如何用StackExchange.Redis连接和操作Redis数据【数据库】
jvm·数据库·python
2301_8148098611 分钟前
PHP源码开发推荐使用哪种机箱_散热与扩展平衡选择【教程】
jvm·数据库·python
Absurd58711 分钟前
SQL分组统计时如何处理文本类型聚合_GROUP_CONCAT的用法
jvm·数据库·python
解救女汉子12 分钟前
如何通过C#读取Oracle数据库中的图片显示到WinForm_BLOB转Byte[]与流处理
jvm·数据库·python
2401_8359568116 分钟前
html标签怎样设置关键词_meta keywords是否仍有效【操作】
jvm·数据库·python
2301_8176722617 分钟前
golang如何调用Twilio语音短信API_golang Twilio语音短信API调用实战
jvm·数据库·python