• lua脚本实现redis分布式锁(脚本解析)


    lua介绍

    Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

    设计目的

    ​ 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

    Lua 特性

    • 轻量级:它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
    • 可扩展:Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
    • 其它特性:
      • 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
      • 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
      • 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
      • 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。

    lua基本语法

    lua脚本感兴趣,请移步到官方教程或者 《菜鸟教程》。这里仅以redis中可能会用到的部分语法作介绍。

    a = 5               -- 全局变量
    local b = 5         -- 局部变量, redis只支持局部变量
    a, b = 10, 2*x      -- 等价于       a=10; b=2*x
    
    • 1
    • 2
    • 3

    流程控制:

    if( 布尔表达式 1)
    then
       --[ 在布尔表达式 1 为 true 时执行该语句块 --]
    elseif( 布尔表达式 2)
    then
       --[ 在布尔表达式 2 为 true 时执行该语句块 --]
    else 
       --[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    redis执行lua脚本 - EVAL指令

    在redis中需要通过eval命令执行lua脚本。

    格式:

    EVAL script numkeys key [key ...] arg [arg ...]
    script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。
    numkeys:lua脚本中KEYS数组的大小
    key [key ...]:KEYS数组中的元素
    arg [arg ...]:ARGV数组中的元素
    
    • 1
    • 2
    • 3
    • 4
    • 5

    案例1:基本案例

    EVAL "return 10" 0
    
    • 1

    输出:(integer) 10

    案例2:动态传参

    EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 10 20 30 40 50 60 70 80 90
    # 输出:10 20 60 70
    
    EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
    # 输出:0
    
    EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 20 10
    # 输出:1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    传入了两个参数10和20,KEYS的长度是1,所以KEYS中有一个元素10,剩余的一个20就是ARGV数组的元素。

    redis.call()中的redis是redis中提供的lua脚本类库,仅在redis环境中可以使用该类库。

    案例3:执行redis类库方法

    set aaa 10  -- 设置一个aaa值为10
    EVAL "return redis.call('get', 'aaa')" 0
    # 通过return把call方法返回给redis客户端,打印:"10"
    
    • 1
    • 2
    • 3

    注意:**脚本里使用的所有键都应该由 KEYS 数组来传递。**但并不是强制性的,代价是这样写出的脚本不能被 Redis 集群所兼容。

    案例4:给redis类库方法动态传参

    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 bbb 20
    
    • 1

    在这里插入图片描述

    学到这里基本可以应付redis分布式锁所需要的脚本知识了。

    案例5:pcall函数的使用(了解)

    -- 当call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,输出错误信息
    EVAL "return redis.call('sets', KEYS[1], ARGV[1]), redis.call('set', KEYS[2], ARGV[2])" 2 bbb ccc 20 30
    -- pcall函数不影响后续指令的执行
    EVAL "return redis.pcall('sets', KEYS[1], ARGV[1]), redis.pcall('set', KEYS[2], ARGV[2])" 2 bbb ccc 20 30
    
    • 1
    • 2
    • 3
    • 4

    注意:set方法写成了sets,肯定会报错。

    在这里插入图片描述

    使用lua保证删除原子性

    删除LUA脚本:

    if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
    
    • 1

    代码实现:

    public void deduct() {
        String uuid = UUID.randomUUID().toString();
        // 加锁setnx
        while (!this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS)) {
            // 重试:循环
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            // this.redisTemplate.expire("lock", 3, TimeUnit.SECONDS);
            // 1. 查询库存信息
            String stock = redisTemplate.opsForValue().get("stock").toString();
    
            // 2. 判断库存是否充足
            if (stock != null && stock.length() != 0) {
                Integer st = Integer.valueOf(stock);
                if (st > 0) {
                    // 3.扣减库存
                    redisTemplate.opsForValue().set("stock", String.valueOf(--st));
                }
            }
        } finally {
            // 先判断是否自己的锁,再解锁
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
            this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock"), uuid);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    压力测试,库存量也没有问题,

  • 相关阅读:
    NPS之Socks流量分析以及未授权复现
    2022年0903在IDEA工具中常见的一些Maven操作<第五课>
    Go sync.Mutex互斥锁的学习
    echarts折线图让某一个点闪烁,echarts闪烁动画。
    中文drupal教程(3)响应对象Response及Cookie设置
    Java 嵌入数据引擎:从 SQLite 到 SPL
    android studio开发app实例-Springboot实现的Android的学习生活交流APP
    数据库系统原理与应用教程(5)—— yum 离线安装 MySQL5.7(Linux 环境)
    来看下这篇文章,教你如何实现一个SpringBoot的Mybatis分库分表组件
    Java虚拟机(JVM)
  • 原文地址:https://blog.csdn.net/qq_39017153/article/details/134281294