当前位置: 首页>后端>正文

一文搞懂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])

加锁流程图如下:


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

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文件:


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

编写代码:

if true then
    return 1
else
    return 2
end

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


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

自动语法提示:


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

选择ifelse:


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

测试

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分布式锁更好,但是会引入额外的开发工作量。


https://www.xamrdz.com/backend/3n71938102.html

相关文章: