
事务与Lua1、事务1.1、命令错误1.2、运行时错误2、Lua用法简述2.1、数据类型及其逻辑处理3、Redis与Lua3.1、在Redis中使用Lua3.2、Lua的Redis API4、案例5、Redis如何管理Lua脚本1、事务熟悉关系型数据库的读者应该对事务比较了解简单地说事务表示一组动作要么全部执行要么全部不执行。例如在社交网站上用户A关注了用户B那么需要在用户A的关注表中加入用户B并且在用户B的粉丝表中添加用户A这两个行为要么全部执行要么全部不执行否则会出现数据不一致的情况。Redis提供了简单的事务功能将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始exec命令代表事务结束它们之间的命令是原子顺序执行的例如下面操作实现了上述用户关注问题。127.0.0.1:6379multi OK127.0.0.1:6379sadd user:a:follow user:b QUEUED127.0.0.1:6379sadd user:b:fans user:a QUEUED可以看到sadd命令此时的返回结果是QUEUED代表命令并没有真正执行而是暂时保存在Redis中。如果此时另一个客户端执行sismember userafollow userb返回结果应该为0。127.0.0.1:6379sismember user:a:follow user:b(integer)0只有当exec执行后用户A关注用户B的行为才算完成如下所示返回的两个结果对应sadd命令。127.0.0.1:6379exec1)(integer)12)(integer)1127.0.0.1:6379sismember user:a:follow user:b(integer)1如果要停止事务的执行可以使用discard命令代替exec命令即可。127.0.0.1:6379discard OK127.0.0.1:6379sismember user:a:follow user:b(integer)0如果事务中的命令出现错误Redis的处理机制也不尽相同。1.1、命令错误例如下面操作错将set写成了sett属于语法错误会造成整个事务无法执行key和counter的值未发生变化127.0.0.1:6388mget key counter1)hello2)100127.0.0.1:6388multi OK127.0.0.1:6388sett key world(error)ERR unknowncommandsett127.0.0.1:6388incr counter QUEUED127.0.0.1:6388exec(error)EXECABORT Transaction discarded because of previous errors.127.0.0.1:6388mget key counter1)hello2)1001.2、运行时错误例如用户B在添加粉丝列表时误把sadd命令写成了zadd命令这种就是运行时命令因为语法是正确的127.0.0.1:6379multi OK127.0.0.1:6379sadd user:a:follow user:b QUEUED127.0.0.1:6379zadd user:b:fans1user:a QUEUED127.0.0.1:6379exec1)(integer)12)(error)WRONGTYPE Operation against a key holding the wrong kind of value127.0.0.1:6379sismember user:a:follow user:b(integer)1可以看到Redis并不支持回滚功能sadd userafollow userb命令已经执行成功开发人员需要自己修复这类问题。有些应用场景需要在事务之前确保事务中的key没有被其他客户端修改过才执行事务否则不执行类似乐观锁。Redis提供了watch命令来解决这类问题下表展示了两个客户端执行命令的时序。可以看到“客户端-1”在执行multi之前执行了watch命令“客户端-2”在“客户端-1”执行exec之前修改了key值造成事务没有执行exec结果为nil整个代码如下所示#T1客户端1127.0.0.1:6379setkeyjavaOK#T2客户端1127.0.0.1:6379watchkey OK#T3客户端1127.0.0.1:6379multi OK#T4客户端2127.0.0.1:6379append key python(integer)11#T5客户端1127.0.0.1:6379append key jedis QUEUED#T6客户端1127.0.0.1:6379exec(nil)#T7客户端1127.0.0.1:6379get keyjavapythonRedis提供了简单的事务之所以说它简单主要是因为它不支持事务中的回滚特性同时无法实现命令之间的逻辑关系计算当然也体现了Redis的“keep it simple”的特性下一小节介绍的Lua脚本同样可以实现事务的相关功能但是功能要强大很多。2、Lua用法简述Lua语言是在1993年由巴西一个大学研究小组发明其设计目标是作为嵌入式程序移植到其他应用程序它是由C语言实现的虽然简单小巧但是功能强大所以许多应用都选用它作为脚本语言尤其是在游戏领域例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中Rovio公司将Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎Web服务器Nginx将Lua语言作为扩展增强自身功能。Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令在这之前必须修改源码。在介绍如何在Redis中使用Lua脚本之前有必要对Lua语言的使用做一个基本的介绍。2.1、数据类型及其逻辑处理Lua语言提供了如下几种数据类型booleans布尔、numbers数值、strings字符串、tables表格和许多高级语言相比相对简单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。(1)字符串下面定义一个字符串类型的数据localstrings valworld其中local代表val是一个局部变量如果没有local代表是全局变量。print函数可以打印出变量的值例如下面代码将打印world其中–是Lua语言的注释。-- 结果是worldprint(hello)(2)数组在Lua中如果要使用类似数组的功能可以用tables类型下面代码使用定义了一个tables类型的变量myArray但和大多数编程语言不同的是Lua的数组下标从1开始计算localtables myArray{redis,jedis, true,88.0}--trueprint(myArray[3])如果想遍历这个数组可以使用for和while这些关键字和许多编程语言是一致的。(a)for下面代码会计算1到100的和关键字for以end作为结束符localintsum0fori1,100dosumsum i end -- 输出结果为5050 print(sum)要遍历myArray首先需要知道tables的长度只需要在变量前加一个#号即可fori1,#myArraydoprint(myArray[i])end除此之外Lua还提供了内置函数ipairs使用for indexvalue ipairs(tables)可以遍历出所有的索引下标和值forindex,valueinipairs(myArray)doprint(index)print(value)end(b)while下面代码同样会计算1到100的和只不过使用的是while循环while循环同样以end作为结束符。localintsum0localint i0whilei100dosumsumi ii 1end --输出结果为5050 print(sum)©if else要确定数组中是否包含了jedis有则打印true注意if以end结尾if后紧跟thenlocaltables myArray{redis,jedis,true,88.0}fori1,#myArraydoifmyArray[i]jedisthenprint(true)breakelse--do nothingendend(3)哈希如果要使用类似哈希的功能同样可以使用tables类型例如下面代码定义了一个tables每个元素包含了key和value其中strings1…string2是将两个字符串进行连接localtables user_1{age28,nametome}--user_1 age is 28print(user_1 age is ..user_1[age])如果要遍历user_1可以使用Lua的内置函数pairsforkey,valueinpairs(user_1)doprint(key..value)end4.函数定义在Lua中函数以function开头以end结尾funcName是函数名中间部分是函数体functionfuncName()…endcontact函数将两个字符串拼接functioncontact(str1,str2)returnstr1..str2end--hello worldprint(contact(hello ,world))3、Redis与Lua3.1、在Redis中使用Lua在Redis中执行Lua脚本有两种方法eval和evalsha。(1)evaleval 脚本内容 key个数 key列表 参数列表下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性127.0.0.1:6379evalreturn hello .. KEYS[1] .. ARGV[1]1redis worldhello redisworld此时KEYS[1]“redis”ARGV[1]“world”所以最终的返回结果是hello redisworld。如果Lua脚本较长还可以使用redis-cli–eval直接执行文件。eval命令和–eval参数本质是一样的客户端如果想执行Lua脚本首先在客户端编写好Lua脚本代码然后把脚本作为字符串发送给服务端服务端会将执行结果返回给客户端整个过程如图所示。(2)evalsha除了使用evalRedis还提供了evalsha命令来执行Lua脚本。如图所示首先要将Lua脚本加载到Redis服务端得到该脚本的SHA1校验和evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容而脚本也会常驻在服务端脚本功能得到了复用。加载脚本script load命令可以将脚本内容加载到Redis内存中例如下面将lua_get.lua加载到Redis中得到SHA1为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”# redis-cli script load $(cat lua_get.lua)7413dc2440db1fea7c0a0bde841fa68eefaf149c执行脚本evalsha的使用方法如下参数使用SHA1值执行逻辑和eval一致。evalsha 脚本SHA1值 key个数 key列表 参数列表所以只需要执行如下操作就可以调用lua_get.lua脚本127.0.0.1:6379evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c1redis worldhello redisworld3.2、Lua的Redis APILua可以使用redis.call函数实现对Redis的访问例如下面代码是Lua使用redis.call调用了Redis的set和get操作redis.call(set,hello,world)redis.call(get,hello)放在Redis的执行效果如下127.0.0.1:6379evalreturn redis.call(get, KEYS[1])1helloworld除此之外Lua还可以使用redis.pcall函数实现对Redis的调用redis.call和redis.pcall的不同在于如果redis.call执行失败那么脚本执行结束会直接返回错误而redis.pcall会忽略错误继续执行脚本所以在实际开发中要根据具体的应用场景进行函数的选择。4、案例Lua脚本功能为Redis开发和运维人员带来如下三个好处Lua脚本在Redis中是原子执行的执行过程中间不会插入其他命令。Lua脚本可以帮助开发和运维人员创造出自己定制的命令并可以将这些命令常驻在Redis内存中实现复用的效果。Lua脚本可以将多条命令一次性打包有效地减少网络开销。下面以一个例子说明Lua脚本的使用当前列表记录着热门用户的id假设这个列表有5个元素如下所示127.0.0.1:6379lrange hot:user:list0-11)user:1:ratio2)user:8:ratio3)user:3:ratio4)user:99:ratio5)user:72:ratiouser{id}ratio代表用户的热度它本身又是一个字符串类型的键127.0.0.1:6379mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio1)9862)7623)5564)4005)101现要求将列表内所有的键对应热度做加1操作并且保证是原子执行此功能可以利用Lua脚本来实现。1)将列表中所有元素取出赋值给mylistlocalmylistredis.call(lrange, KEYS[1],0, -1)2)定义局部变量count0这个count就是最后incr的总次数localcount03)遍历mylist中所有元素每次做完count自增最后返回countforindex,keyinipairs(mylist)doredis.call(incr,key)countcount 1endreturncount将上述脚本写入lrange_and_mincr.lua文件中并执行如下操作返回结果为5。redis-cli--evallrange_and_mincr.lua hot:user:list(integer)5执行后所有用户的热度自增1127.0.0.1:6379mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio1)9872)7633)5574)4015)102本节给出的只是一个简单的例子在实际开发中开发人员可以发挥自己的想象力创造出更多新的命令。5、Redis如何管理Lua脚本Redis提供了4个命令实现对Lua脚本的管理下面分别介绍。(1)script loadscript load script此命令用于将Lua脚本加载到Redis内存中前面已经介绍并使用过了这里不再赘述。(2)script existsscripts exists sha1[sha1 …]此命令用于判断sha1是否已经加载到Redis内存中127.0.0.1:6379script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc51)(integer)1(3)script flushscript flush此命令用于清除Redis内存已经加载的所有Lua脚本在执行script flush后a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在127.0.0.1:6379script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc51)(integer)1127.0.0.1:6379script flush OK127.0.0.1:6379script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc51)(integer)0(4)script killscriptkill此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时甚至Lua脚本存在问题那么此时Lua脚本的执行会阻塞Redis直到脚本执行完毕或者外部进行干预将其结束。下面我们模拟一个Lua脚本阻塞的情况进行说明。下面的代码会使Lua进入死循环while11doend执行Lua脚本当前客户端会阻塞127.0.0.1:6379evalwhile 11 do end0Redis提供了一个lua-time-limit参数默认是5秒它是Lua脚本的“超时时间”但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后向其他命令调用发送BUSY的信号但是并不会停止掉服务端和客户端的脚本执行所以当达到lua-time-limit值之后其他客户端在执行正常的命令时将会收到“Busy Redis is busyrunning a script”错误并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本127.0.0.1:6379get hello(error)BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.此时Redis已经阻塞无法处理正常的调用这时可以选择继续等待但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适所以选择script kill当script kill执行之后客户端调用会恢复127.0.0.1:6379scriptkillOK127.0.0.1:6379get helloworld但是有一点需要注意如果当前Lua脚本正在执行写操作那么script kill将不会生效。例如我们模拟一个不停的写操作while11doredis.call(set,k,v)end此时如果执行script kill会收到如下异常信息(error)UNKILLABLE Sorry the script already executedwritecommands against the dataset. You can eitherwaitthe script termination orkillthe serverina hard way using the SHUTDOWN NOSAVE command.上面提示Lua脚本正在向Redis执行写命令要么等待脚本执行结束要么使用shutdown save停掉Redis服务。可见Lua脚本虽然好用但是使用不当破坏性也是难以想象的。