黑马点评-分布式锁Lua脚本

文章目录

分布式锁

当我们的项目服务器不只是一台(单体),而是部署在多态服务器上(集群/分布式),同样会出现线程安全问题。不同服务器内部有不同的JVM,每个JVM里面有不同的锁监视器,每一个锁都可以有一个线程获取。

所以我们要想办法让多个JVM共享同一个锁监视器。

分布式锁:

满足分布式系统或集群模式下多进程可见 并且互斥的锁,主要有3种方式:

  • MySQL:

    利用数据库的 唯一索引约束行锁 特性,确保同一时刻只有一个进程能获取锁。

    sql 复制代码
    //创建锁
    CREATE TABLE distributed_lock (
        id INT PRIMARY KEY AUTO_INCREMENT,
        lock_key VARCHAR(64) UNIQUE, -- 唯一约束
        owner VARCHAR(64),          -- 锁持有者标识
        expire_time DATETIME        -- 锁过期时间
    );
    
    //获取锁
    -- 尝试插入锁记录(唯一键冲突则失败)
    INSERT INTO distributed_lock (lock_key, owner, expire_time) 
    VALUES ('resource_lock', 'client_123', NOW() + INTERVAL 30 SECOND);
    //若插入成功,表示获取锁;若失败(唯一键冲突),则锁被其他进程占用。
    
    
    //释放锁
    DELETE FROM distributed_lock 
    WHERE lock_key = 'resource_lock' AND owner = 'client_123';
    
    
    //锁超时处理:
    DELETE FROM distributed_lock WHERE expire_time < NOW();
  • Redis

    利用 Redis 的setnx实现锁的互斥性和自动过期。

    SETNX key value

    指定的key不存在时,将key的值设为value,如果设置成功返回1,如果key已经存在,就不做任何操作,返回0。

    多个客户端同时尝试设置同一个key,只有一个能成功获取锁(非阻塞:没获取到就放弃而不是等待),从而获得锁。

    也给锁设置过期时间,到时间自动释放,避免死锁发生

  • Zookeeper:

    利用 ZooKeeper 的 临时顺序节点 实现锁的互斥和自动释放。

    有序性:线程来,每次创建一个临时节点,节点IP递增,约定id最小的哪个算他获取锁成功。

    唯一性:所有线程都创建相同名称的节点,同样也只有一个能获取成功。

    这个知识点就不在这里展开讲述。

我们采用redis:

Redis setnx
java 复制代码
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

private boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
  • 这里为什么返回的是BooleanUtil.isTrue(flag)而不直接是flag?

    Boolean是一个包装类,它可以是true、false或者null。

    setIfAbsent操作在成功设置键时返回true,如果键已经存在则返回false,redis连接异常时,可能会返回null。

    当flag为null时,自动拆箱(Boolean->boolean)会抛出NullPointerException

    BooleanUtil.isTrue:用于安全地将Boolean对象转换为boolean值,处理null的情况,例如,当flag为null时,该方法会返回false。

代码:

java 复制代码
 // 5.一人一单
        Long userId = UserHolder.getUser().getId();

        // 创建锁对象
        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 尝试获取锁
        boolean isLock = redisLock.tryLock(1200);
        // 判断
        if(!isLock){
            // 获取锁失败,直接返回失败或者重试
            return Result.fail("不允许重复下单!");
        }

        try {
            //略
        } finally {
            // 释放锁
            redisLock.unlock();
        }
redis锁误删

在某些情况下,上面也会出现问题:

当线程1获取到redis锁后由于某种原因阻塞时间过长 ,时间超过了redis设置的过期时间,那么redis锁自动释放。这个时候线程2来获取这个锁成功了,这个时候线程1的业务完成了,线程1就直接把这个redis锁给释放了,而线程2还在执行自己的业务,这是线程3也来获取锁成功,也执行业务,可能再次发生线程安全问题。

所以我们的解决方案就是:在释放锁之前通过线程标识查看是否是自己的锁

代码:

java 复制代码
//使用UUID(不同JVM,UUID不同)
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

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

@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);
    }
    
    
    @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);
        }
    }
Lua脚本

但是,同样也存在问题:当线程1判断了锁是自己的后,要准备释放这个锁的时候发生了阻塞(JVM内部垃圾回收),锁过期,线程2来获得,线程1恢复正常后直接就释放锁。

所以,我们要确保判断+释放锁是一个原子性操作

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

Lua 教程 | 菜鸟教程

静态脚本:

不写死的话,用参数:

Lua如何结合到java里:

  • 创建脚本

    lua 复制代码
    -- 比较线程标示与锁中的标示是否一致
    if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
        -- 释放锁 del key
        return redis.call('del', KEYS[1])
    end
    return 0
  • 读取脚本文件

    lua 复制代码
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
        static {
            UNLOCK_SCRIPT = new DefaultRedisScript<>();
            UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
            UNLOCK_SCRIPT.setResultType(Long.class);
        }
  • execute()调用脚本

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

不断学习中,感谢大家的观看>W<

相关推荐
老神在在0014 小时前
javaEE1
java·开发语言·学习·java-ee
魔道不误砍柴功4 小时前
《接口和抽象类到底怎么选?设计原则与经典误区解析》
java·开发语言
small_white_robot5 小时前
Tomcat- AJP协议文件读取/命令执行漏洞(幽灵猫复现)详细步骤
java·linux·网络·安全·web安全·网络安全·tomcat
图梓灵6 小时前
Maven与Spring核心技术解析:构建管理、依赖注入与应用实践
java·笔记·spring·maven
岁忧6 小时前
(nice!!!)(LeetCode 每日一题) 3372. 连接两棵树后最大目标节点数目 I (贪心+深度优先搜索dfs)
java·c++·算法·leetcode·go·深度优先
加什么瓦7 小时前
Java—多线程
java·开发语言
野木香7 小时前
idea使用笔记
java·笔记·intellij-idea
bing_1587 小时前
在 Spring Boot 项目中如何合理使用懒加载?
java·spring boot·后端
CN.LG7 小时前
MyBatis 的动态 SQL 特性来实现有值字段才进行插入或更新
java·sql·mybatis
lyh13448 小时前
【JavaScript 性能优化方法】
java