上文中我们使用了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])
加锁流程图如下:
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文件:
编写代码:
if true then
return 1
else
return 2
end
推荐使用Idea的lua插件,可以有丰富的关键字提示。
自动语法提示:
选择ifelse:
测试
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
分布式锁更好,但是会引入额外的开发工作量。