一文搞懂Redis分布式锁-加餐(一)lua脚本扣减库存

上文中我们使用了redisson进行扣减库存的操作,这样的实现方式已经可以解决我们对于分布式锁的需求。但是大家思考一下,redisson是如何实现分布式锁的?接下来我们回顾一下redisson的源码。
以下是redisson关于加锁的部分代码:

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

从上面可以看出来,redisson其实是发送了一段代码到redis中执行,这段语句是使用lua这一小巧精炼的语言编写的。

Lua语言简介

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。

Lua 特性

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

Lua 应用场景

  • 游戏开发
  • 独立应用脚本
  • Web 应用脚本
  • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系统,如入侵检测系统

基本语法

这里只列举在上文中用到的语法,详细语法可以查询https://www.runoob.com/lua/lua-tutorial.html

if语句

if(true)
then
    print("false")
end

代码详解

--判断hash是否存在,KEYS[1]代表从KEYS这个参数数组中取出第一个元素(Lua中元素下标从1开始)
if (redis.call('exists', KEYS[1]) == 0) 
then 
      --如果hash不存在
      --调用hincrby,将hash中的value值加1(实现可重入)
      redis.call('hincrby', KEYS[1], ARGV[2], 1); 
      --设置失效时间
      redis.call('pexpire', KEYS[1], ARGV[1]);
      return nil; 
end; 
 --如果hash存在,判断hash中对应的field是否存在
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
then
      --存在,加1并设置失效时间
      redis.call('hincrby', KEYS[1], ARGV[2], 1);
      redis.call('pexpire', KEYS[1], ARGV[1]);
      return nil
end
--返回失效时间
return redis.call('pttl', KEYS[1])

加锁流程图如下:


image.png

hash中存放线程ID,如果锁存在,再次加锁,只有线程ID匹配的线程可以加锁,否则会返回失败。重复加锁的含义是设计可重入锁,解锁时需要将value清理为0才可以成功解锁。

编写第一个Redis的Lua脚本

环境搭建

添加Lua脚本读取工具:

package com.brianxia.redislock.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.Serializable;

@Configuration
public class RedisConfig {

    @Bean
    public DefaultRedisScript<String> defaultRedisScript() {
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(String.class);
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/demo.lua")));
        return defaultRedisScript;
    }
}

创建ResourceScriptSource对象时,可以指定lua脚本的读取目录

编写lua脚本测试

在resources下创建lua文件:


image.png

编写代码:

if true then
    return 1
else
    return 2
end

推荐使用Idea的lua插件,可以有丰富的关键字提示。


image.png

自动语法提示:


image.png

选择ifelse:


image.png

测试

package com.brianxia.redislock;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

@SpringBootTest
class RedisLockApplicationTests {

    @Autowired
    DefaultRedisScript<String> redisScript;
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        Object res = redisTemplate.execute((RedisConnection connection) -> connection.eval(
                redisScript.getScriptAsString().getBytes(),
                ReturnType.INTEGER,
                0));
        System.out.println(res);
    }

}

最后看到控制台打印出1,测试成功。

Lua参数详解

Redis中允许传递动态参数,参数分为两种Keys和Args,看下面的接口定义:

    @Override
    public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) {
        return convertAndReturn(delegate.eval(script, returnType, numKeys, keysAndArgs), identityConverter);
    }
  • numKeys 传递的keys参数数量
  • keysAndArgs 可以传递多个参数
    比如,keysAndArgs里一共传递了5个参数,numKeys 设定为2,那么前2个参数是keys后3个参数是args。

测试

package com.brianxia.redislock;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.nio.charset.StandardCharsets;

@SpringBootTest
class RedisLockApplicationTests {

    @Autowired
    DefaultRedisScript<String> redisScript;
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
//传递5个参数,2个keys,3个args
        Object res = redisTemplate.execute((RedisConnection connection) -> connection.eval(
                redisScript.getScriptAsString().getBytes(),
                ReturnType.INTEGER,
                2
                ,"1".getBytes()
                ,"2".getBytes()
                ,"3".getBytes()
                ,"4".getBytes()
                ,"5".getBytes()));
        System.out.println(res);
    }

}

lua脚本:

注意lua脚本中获取args使用的是ARGV数组

print(KEYS[1])
print(KEYS[2])
print(ARGV[1])
print(ARGV[2])
print(ARGV[3])

测试发现redis-server控制台中输入1,2,3,4,5,证明参数读取无误。

扣减库存操作代码编写

-- 1.获取库存数量
local stock=tonumber(redis.call('get',KEYS[1]))
-- 2.转换成数值型
local count=tonumber(ARGV[1])
if
-- 2.如果库存小于扣减量,库存不足,返回-1
    stock<count then return -1
end
-- 3.如果库存大于等于扣减量,扣减库存
   stock =stock-count
   redis.call('set',KEYS[1],tostring(stock))
return stock

java代码:

package com.brianxia.redislock.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author brianxia
 * @version 1.0
 * @date 2020/12/14 16:20
 */
@RestController
public class LuaController {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    DefaultRedisScript<String> redisScript;

    @RequestMapping("/testlua")
    public void testLua(){

        int count = 50;
        String key = "stock";
        Object res = redisTemplate.execute((RedisConnection connection) -> connection.eval(
                redisScript.getScriptAsString().getBytes(),
                ReturnType.INTEGER,
                1,
                key.getBytes(),
                String.valueOf(count).getBytes()));
        Long result= (Long) res;
        if(result == -1){
            System.out.println("操作失败,库存不足");
        }else if(result == 0){
            System.out.println("库存已被扣减为0");
        }else{
            System.out.println("交易成功了,库存还有:" + result);
        }

    }
}

总结

使用lua编写扣减库存操作,由于操作是原子性的,不存在线程安全问题,性能比redisson分布式锁更好,但是会引入额外的开发工作量。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容