Redis+Lua脚本实现复合操作原子化

  • Post author:
  • Post category:其他




一、引言

Redis是高性能的key-value数据库,在很大程度克服了memcached这类key/value存储的不足,在部分场景下,是对关系数据库的良好补充。得益于超高性能和丰富的数据结构,Redis已成为当前架构设计中的首选key-value存储系统。

虽然Redis官网上提供了200多个命令,但做程序设计时还是避免不了为了实现一小步业务逻辑而多次调用Redis的情况。

分布式redis计算,要满足实现redis操作在同一个应用,redis多不操作一次性调用完成,这种分散操作无法利用Redis的原子特性,占用多次网络IO。要满足redis多步操作原子化,需要使用redis+lua脚本的方式实现,。这种分散操作无法利用Redis的原子特性,占用多次网络IO。

流量控制/配额管控最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua进行实现,通过这种技术可以实现的高并发和高性能。



二、Redis与Lua

Lua 是一个小巧的脚本语言,几乎可以运行在所有操作系统和平台上。我们一般不会用Lua处理特别复杂的事务,因此只需了解一些lua的基本语法即可。

Redis问世之后,其开发者也意识到了开篇提到的问题,因此Redis从2.6版本开始支持Lua脚本。新版本的Redis还支持Lua Script debug,感兴趣的小伙伴可以去官网的Documentation中找到对应介绍和QuickStart。

有了Lua脚本之后,使用Redis程序时便能够在以下方面实现显著提升:

  • 减少网络开销:本来N次网络请求的操作,可以用一个请求完成。原先N次请求的逻辑放在Redis服务器上完成,减少了网络往返时延;
  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。这是一个重要特性,一定要拿小本本记好。至于为什么是一个原子操作,我们以后再分析;
  • 复用:客户端发送的脚本会永久存储在Redis中。这样其他客户端就可以复用这一脚本,而不需要使用代码完成同样的逻辑。

    所以现在流传一句话:要想学好Redis,必会Lua Script。



三、Redis执行Lua脚本



3.1Redis的EVAL

Redis 127.0.0.1:6379> EVAL script  numkeys key [key ...] arg [arg ...]
  • script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个Lua函数。
  • numkeys: 用于指定键名参数的个数。
  • key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的Redis键(key)。在Lua中,这些键名参数可以通过全局变量 KEYS 数组,用1为基址的形式访问( KEYS[1] ,KEYS[2],依次类推)。
  • arg [arg …]: 附加参数,在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。



3.2 执行脚本文件和缓存脚本

如果只能在命令行中写脚本执行,遇到复杂的脚本程序岂不是会抓狂?

下面我们来看一下,如何让Redis执行Lua脚本文件,同时也验证一下lua脚本的复用特性(以后我们再也不需要定期批量删除某些符合特定规则的key了)。

Redis 127.0.0.1:6379> SCRIPT LOAD  script
Redis 127.0.0.1:6379> EVALSHA sha1  numkeys key [key ...] arg [arg ...]

Redis提供了一个SCRIPTLOAD命令,命令后面的script即为Lua脚本。命令将脚本script添加到脚本缓存中,但并不立即执行这个脚本。执行命令后,Redis会返回一个SHA1串,第二个EVALSHA命令即可执行。

需要注意的是,脚本可以在缓存中保留无限长的时间,直到执行完SCRIPT FLUSH。



3.3 Java中执行redis+lua脚本

public Object eval(final String script, final int keyCount, final String... params) 
public Object eval(final String script, String sampleKey) 
public Object eval(final String script, final List<String> keys, final List<String> args) 



3.4应用中的使用:

public Long limitRate(String key,String timeout){		
	
	Object value = jedisCluster.eval(limitScript,1,key,timeout);
	return Long.valueOf(value.toString());
}

public Long quotaRate(String key,String threshold,String timeout){		
	
	Object value = jedisCluster.eval(quotaScript,1,key,timeout,threshold);
	return Long.valueOf(value.toString());
}	

lua脚本:

limitScript:

local times = redis.call('incr',KEYS[1])
if times == 1 then
    redis.call('expire',KEYS[1], ARGV[1])
end
	
return times

quotaScript:

 if redis.call('EXISTS',KEYS[1]) == 1 then
	local valueStr = redis.call('get',KEYS[1])
	
	local value = tonumber(valueStr)
	local maxCount = tonumber(ARGV[2])
	if value >  maxCount then
		return value
	end
	
	local times = redis.call('incr',KEYS[1])
	return times
else
	local times = redis.call('incr',KEYS[1])
	if times == 1 then
	    redis.call('expire',KEYS[1], ARGV[1])
	end
	return times
end



版权声明:本文为weixin_39522897原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。