Jedis与lua脚本——lua脚本在redis中的使用


作者:空白

1.使用lua的几个好处

原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,就不需要在本地使用繁琐的事务或者使用锁机制,任何能在事务机制下能完成的任务lua脚本也能做到。

重复使用。lua脚本能存放到redis服务器,作为一个redis扩展命令而存在着,每次脚本的执行只需要传入脚本在redis服务器中的唯一id就能执行该脚本扩展的原子命令。

降低网络开销。可以将多个命令通过脚本的形式一次发送,不用每个命令都逐个发送,减少网络延迟。

2.redis原生命令以及Jedis执行lua脚本的两种API方法

redis原生命令

  • eval :执行lua脚本,需要传入lua脚本代码
  • evalsha :执行lua脚本,需要传入lua脚本在redis内的编号
  • script load : 将lua脚本缓存到redis服务器,并返回该脚本在redis的编号

eval直接执行脚本示例如下

EVAL script numkeys key [key ...] arg [arg ...]

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua脚本简单描述 eval 命令的参数 依次为:lua脚本 ,key的数量为2, 第一个key值,第二个key值,第一个其他参数,第二个其他参数。 返回的是字符串数组。
注意:redis客户端与服务器交换的数据最终转换称字符串的形式进行发送和接收。

script load和evalsha 每次执行eval命令都会将脚本传入到redis中,如果脚本执行频率很高的话,就会增加网络开销 因此redis提供了缓存脚本的命令script load 再调用evalsha 执行脚本,每次执行evalsha命令都是传入脚本在redis服务器的hash值,减少了脚本传输的网络开销。两个命令的使用如下:

SCRIPT LOAD SCRIPT
EVALSHA sha numkeys key [key ...] arg [arg ...]

redis> SCRIPT LOAD "return 'hello world'"
"34343d4s34343434a5656544534434da343ad4454a"

redis> EVALSHA 34343d4s34343434a5656544534434da343ad4454a 0
"hello world"

SCRIPT LOAD命令的返回值要保存好,作为脚本id来查找缓存在redis里的脚本。 EVALSHA 参数和eval参数结构一样,只是第一个参数不是脚本内容,而是脚本缓存在redis中的唯一id。

Jedis执行lua脚本的两种API方法

  • eval(String luaStr ,Object[] keyParams , Object[] arvgs)
  • scriptLoad(String luaStr)
  • evalsha(String luaLoad ,Object[] keyParams , Object[] arvgs)

lua脚本实现同一个IP的访问限流

--在lua脚本内调用redis命令incr,实现第一个key参数的自增
--如果key不存在则创建key并赋值为1,如果存在则自动增加1,返回最终的值赋给变量num
local num=redis.call('incr',KEYS[1])
--判断变量num的值,如果为1 则设置这个key的过期时间为ARGV[1] ,并返回成功1
if tonumber(num)==1 then 
  redis.call('pexpire',KEYS[1],ARGV[1])
  return 1
--如果num的值大于第二个值参数,则返回失败0 ,小于的话就返回成功1
elseif tonumber(num)>tonumber(ARGV[2]) then
  return 0
else
  return 1
end

jedis两种API使用方式Github源码demo

3.lua分布式锁
对于分布式锁,可以理解为是两个不相关的进程之间的通信,业界有很多成熟的分布式锁工具。redis+lua实现轻量的分布式锁,使得不同进程之间相互协作,实现高并发的安全性。因为lua脚本的原子性和快速等特性,多个进程之间协助速度将会非常的快。 分布式锁也具有锁的一些特性,每个进程都一定会有获取锁,释放锁这两个过程,有些不稳定的进程,还会出现成功获取锁后还没来得及释放锁就死亡了的问题,以及进程获取锁失败需要在固定时间内多次去尝试获取锁需求,这些都是分布式锁面临的问题。如下:

  • 获取锁的进程死亡
  • 获取锁失败进程尝试多久

redis实现分布式锁,有很多可以实现锁的命令,我选的是setnx命令:redis中存在key则命令执行失败,否则key创建成功。

获取锁的进程死亡

如果只使用这个命令来实现分布式锁,则会出现上面描述的第一个问题,进程还未释放锁就死了,那么其他进程就永远无法成功获取锁,那为了避免这个问题,根据redis功能,可以给key设置过期时间,就算进程死了,过了时间锁就不存在了,那其他进程就能获取到锁了。

获取锁失败进程尝试多久

如果每次执行setnx命令失败,就算是获取锁失败的话,就会使得获取锁的成功率大大降低,为了避免这个问题,就需要在获取锁的逻辑中增加多次尝试的机制: 在规定的延迟范围内,定时的多次尝试执行setnx命令,如果还没能执行成功,则算是获取锁失败,这种处理能大大提高获取锁的成功率。

博主开发的一个基于redis+lua脚本的轻量分布式锁 JackDKingLuaDistributedKey 就解决了这些问题,感兴趣的同学可以用于工作项目中,或作为学习参考的demo

点击跳转到Github源码地址

获取锁的lua脚本

--获取锁key和锁设置的过期时间expireTime 
local key = KEYS[1]
local expireTime = ARGV[1]

#这个value可以作为扩展的点,你可以保存获取锁的进程唯一标识。
local value = ARGV[1] 

--使用redis的setnx命令加锁 
local result = redis.call('setnx',key,value)

if result == 1 
then 
	-- 加锁成功
	--expireResult==0的情况是为了支持Redis versions <2.1.3情形下,过期时间存在则不会设置,之后的版本则会覆盖过期时间
	local res = redis.call('expire',key,expireTime)
	if res == 1
	then
		return 1
	else
		return 0
	end
else
	return 0
end

获取分布式锁不仅仅只是执行lua脚本


JackDKingLuaDistributedKey分布式锁获取锁 核心逻辑
 public static boolean lock(String key , Long expireSeconds,Long waitSeconds)
	{
		boolean result ;
		result = setNx(key, expireSeconds);//第一次执行 获取锁的lua脚本,成功则直接返回,失败则进入 超时重试流程:有限时间内 重试执行lua脚本。
		if(result)return result;
		else {
			//锁获取开始时间点
			Long curent = System.currentTimeMillis();
			//锁获取结束时间点
			Long outTime = curent + waitSeconds;

			//对比当前时间是否还在过期时间点内,如果 获取分布式锁 还在规定的时间内,则继续执行lua脚本
			while(System.currentTimeMillis()<outTime) {
				boolean resultboolean =  setNx(key, expireSeconds);
				if(resultboolean)return resultboolean;
				try {
					Thread.sleep(5);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(" 锁获取超时,失败");
		}
		
		return false;
		
	}

重试机制流程的活动图如下:建议拖拽图片到新的浏览器窗口查看,带来不便十分抱歉alt 阶段小结:
1.为key设置的过期时间 解决了获取锁的进程死亡的问题。

2.执行lua脚本失败 情况下增加 一定时间内重试机制 增加了分布式锁获取的成功率

4.lua高并发减库存
描述:高并发减库存的几个经典的场景,抢红包,秒杀,抢票,电商活动等。这些场景下 都是在短时间内有大量的流量请求涌进来,有时候1s内有好几万个请求对一个数据进行减量,这个时候除了超快的响应性能,最重要的是还要保证数据的一致性。这都是作为一个合格的架构师应该考虑的范畴,即系统性能以及可靠性。

问题:短时间内大量流量的涌入,对于这种高并发场景,我们最好的工具就是redis,官网宣传redis能处理能力达到每秒10万条命令。那么对于这种响应性能问题就不存在了,但是我们还需要解决数据一致性的问题,保证所有被成功处理的请求的减库总额和 库存的真实减少量保证一致,否则 超额发出的红包,商品 这些损失都必须由平台报销,这种情况是必须要控制住的。

方案:对于刚才的响应能力,redis的性能就能够满足。但是对于数据的一致性要求,我们还是要借助于redis的lua脚本开发。对于减库存这个过程,我们能将它分离为两个子过程,减少额与现存库存的比较,如果库存少于请求的减少额,则请求处理失败,否则将库存量减去请求的减库存量,得到最终的库存数量。简单来说就是先判断库存够不够,够就减,不够就失败。

方案实现github实例的传送门

实例描述

  • 减库存的lua脚本

    1)获取 key参数 以及值参数 2)判断库存量是否足够 3)将成功订单和减少额拼接在一起 orderid:amt 字符串 push到成功订单队列中去,供平台系统其他异步业务执行。

--获取参数模块
--库存商品的唯一 key
local key = KEYS[1]

--获取减库存请求订单的订单号
local orderid = KEYS[2]

--要减掉库存的数量 
local reduceValue = ARGV[1]
--类型转换
local reduceNumber = tonumber(reduceValue)


--比较模块,比较现存库存数量是否大于扣减量
--获取库现有存量
local result = redis.call('get',key)
--将result的string类型转换成数字类型
local resultNumber = tonumber(result)

--减库存后 将成功订单放入到list中,list名称
local listName = "orderlist"

--比较现有库存量和扣减量
if(resultNumber<reduceNumber)
then 
	return -1    -- 库存量不足,减库存失败

else
	--进行减库存操作。
	local finalNumber = redis.call('DECRBY',key,reduceNumber)
	local reducevalue = tostring(reduceNumber)
	--将成功减库存的订单号和 减少的库存量 用冒号拼接在一起 作为value放入到异步队列中
	--异步队列 就被 其他系统执行
	local listvalue = orderid..":"..reducevalue
	--将减库存信息放入到 出货队列中去。
	redis.call('lpush',listName,listvalue)
	
	return finalNumber
end
	

实例测试代码

  public static void main(String[] args) {
    	
//    	setStock("testkey",1000L);
    	System.out.println("success");
    	System.out.println(reduceStock("testkey", System.currentTimeMillis(), 10L));
	}

输出结果

success
960

redis成功订单队列数据:

HostIP:6379> lrange orderlist 0 -1
1) "1584011213372:10"
2) "1584011209126:10"
3) "1584011206433:10"
4) "1584011184144:10"

对于redis队列中成功队列的数据,系统异步去处理,例如 出库,物流等相关系统。

5.lua高并发限流器
描述: 任何一个服务作为服务提供方,为了保证服务的可用性和安全性,不能任由外界涌入大量的请求,需要对其他系统的请求进行资源限流管理。市场上有很多好用的限流工具,jdk自带的semaphore和guava的RateLimiter。在这里讲述的基于redis+lua脚本的限流方案。

方案实例:

1. lua脚本

--将限流key 的值自增 1,并返回自增后的值
local num=redis.call('incr',KEYS[1])
--判断自增后值的大小,如果值等于1:表示这是第一次访问,则设定一个过期时间,并返回一个值
if tonumber(num)==1 then
	redis.call('pexpire',KEYS[1],ARGV[1])
	return 1
--如果值大于设定的流量阙值 ARGV[2], 则表示 流量请求数量超过阙值,则返回失败 0
elseif tonumber(num)>tonumber(ARGV[2]) then
	return 0
	
--如果返回的值在1到阙值之间(包括阙值),则返回成功1
else 
	return 1
end

2.脚本测试的核心代码

		 System.out.println("进入方法");
	        List<String> keys = new ArrayList<>();
               //根据发起请求的主机ip来 限流
	        keys.add("ip:limit:127.0.0.1");
	        List<String> arggs = new ArrayList<>();
	        arggs.add("6000");//限流的时间范围
	        arggs.add("4");//限流  数字为4 
	        //两个参数的含义: 6s内 只允许 4个请求 通过。
	        //下面方法是返回  lua脚本在redis中的缓存id。后续通过id来执行脚本,避免每次执行都传  lua 代码
        	String luaLoad = jedis.scriptLoad(lua);
        	System.out.println("lua脚本缓存在redis中的id是:"+luaLoad);
        	System.out.println("开始每500ms发起一次 redis请求");
	        for(int i =0 ; ; i++)
	        {
		        Object obj = jedis.evalsha(luaLoad,keys,arggs);
		        System.out.println("第"+i+"个500ms时刻执行脚本返回结果:"+obj);
		        try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
	        }
			
		

小结:测试代码-限制主机127.0.0.1 在每个6s时间内 访问次数不能大于4。请求每隔500ms发起一次,那么6s内就有4个以上的请求,但实际被接受的请求只有4个。测试运行效果如下:

进入方法
lua脚本缓存在redis中的id是:69e084e80c552b36accdb2f9f65f82e5f7e7e1d2
开始每500ms发起一次 redis请求
第0个500ms时刻执行脚本返回结果:1
第1个500ms时刻执行脚本返回结果:1
第2个500ms时刻执行脚本返回结果:1
第3个500ms时刻执行脚本返回结果:1
第4个500ms时刻执行脚本返回结果:0
第5个500ms时刻执行脚本返回结果:0
第6个500ms时刻执行脚本返回结果:0
第7个500ms时刻执行脚本返回结果:0
第8个500ms时刻执行脚本返回结果:0
第9个500ms时刻执行脚本返回结果:1
第10个500ms时刻执行脚本返回结果:1
第11个500ms时刻执行脚本返回结果:1
第12个500ms时刻执行脚本返回结果:1
第13个500ms时刻执行脚本返回结果:0
第14个500ms时刻执行脚本返回结果:0
第15个500ms时刻执行脚本返回结果:0
第16个500ms时刻执行脚本返回结果:1
第17个500ms时刻执行脚本返回结果:1
第18个500ms时刻执行脚本返回结果:1
第19个500ms时刻执行脚本返回结果:1
第20个500ms时刻执行脚本返回结果:0
第21个500ms时刻执行脚本返回结果:0
第22个500ms时刻执行脚本返回结果:0
第23个500ms时刻执行脚本返回结果:0
第24个500ms时刻执行脚本返回结果:1
第25个500ms时刻执行脚本返回结果:1
第26个500ms时刻执行脚本返回结果:1
第27个500ms时刻执行脚本返回结果:1
第28个500ms时刻执行脚本返回结果:0
第29个500ms时刻执行脚本返回结果:0
第30个500ms时刻执行脚本返回结果:0
第31个500ms时刻执行脚本返回结果:0
第32个500ms时刻执行脚本返回结果:0

该限流脚本参数解释:

键参数:发起请求的主机唯一标志,一般用主机的ip表示

值参数:第一个表示时间范围,第二个表示限制的次数,即 在这个时间范围内的 总请求次数。 redis+lua限流器GitHub实例demo

扫码或搜索:前沿科技
发送 290992
即可立即永久解锁本站全部文章
  1. 这个屌! 感谢 站主输出,我会持续关注的!