Lua脚本
Redis2.6之后新增的功能,我们可以在redis中通过lua脚本操作redis。与事务不同的是事务是将多个命令添加到一个执行的集合,执行的时候仍然是多个命令,会受到其他客户端的影响,而脚本会将多个命令和操作当成一个命令在redis中执行,也就是说该脚本在执行的过程中,不会被任何其他脚本或命令打断干扰。正是因此这种原子性,lua脚本才可以代替multi和exec的事务功能。同时也是因此,在lua脚本中不宜进行过大的开销操作,避免影响后续的其他请求的正常执行。
使用lua脚本的好处
lua脚本是作为一个整体执行的.所以中间不会被其他命令插入 可以把多条命令一次性打包,所以可以有效减少网络开销; lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用.也减少了代码量
应用
redis脚本使用eval命令执行lua脚本,其中numkeys表示lua script里有多少个key参数,redis脚本根据该数字从后面的key和arg中取前n个作为key参数,之后的都作为arg参数:
eval script numkeys key [key ...] arg [arg ...]
在redis里使用eval命令调用lua脚本,且该脚本在redis里作为单条命令去执行不会受到其余命令的影响,非常适用于高并发场景下的事务处理。同样我们可以在lua脚本里实现任何想要实现的功能,迭代,循环,判断,赋值 都是可以的。
lua脚本缓存
redis脚本也支持将脚本进行持久化,这样的话,下次再使用就不用输入那么长的lua脚本了。事实上使用eval执行的时候也会缓存,eval与load不同的是eval会将lua脚本执行并缓存,而load只会将脚本缓存。相同点是它们都使用sha算法进行缓存,因此只要lua脚本内容相同,eval与load缓存的sha码就是一样的。而缓存后的脚本,我们可以使用evalsha命令直接调用,极大的简化了我们的代码量,不用重复的将lua脚本写出来。 #eval 执行脚本并缓存 eval script numkeys key [key ...] arg [arg ...]
#load 缓存lua脚本
SCRIPT LOAD script
#使用缓存的脚本sha码调用脚本
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
#使用sha码判断脚本是否已缓存
SCRIPT EXISTS sha1 [sha1 ...]
#清空所有缓存的脚本
SCRIPT FLUSH
#杀死当前正在执行的所有lua脚本
SCRIPT KILL
Pipeline:管道
Redis提供了批量操作命令(例如mget,mset等),有效的节约RTT.但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall存在,需要消耗n次RTT.Redis的客户端和服务端可能不是在不同的机器上.例如客户端在北京,Redis服务端在上海,两地直线距离为1300公里,那么1次RTT时间=1300×2/(300000×2/3)=13毫秒(光在真空中传输速度为每秒30万公里,这里假设光纤的速度为光速的2/3),那么客户端在1秒内大约只能执行80次左右的命令,这个和Redis的高并发高吞吐背道而驰.
Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令按照顺序执行并装填结果返回给客户端.未使用Pipeline执行了n次命令,整个过程需要n个RTT.
Pipeline并不是什么新的技术和机制,很多技术上都使用过.而且RTT在不同网络环境下会有不同,例如同机房和同机器会比较快,跨机房跨地区会比较慢.Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法.
2. 原生批量命令与Pipeline对比
可以使用Pipeline模拟出批量操作的效果,但是在使用时需要质疑它与原生批量命令的区别,具体包含几点:
原生批量命令是原子性,Pipeline是非原子性的. 原生批量命令是一个命令对应多个key,Pipeline支持多个命令. 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端与客户端的共同实现.
3. Pipeline总结
Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时机,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成.
Pipeline只能操作一个Redis实例,但即使在分布式Redis场景中,也可以作为批量操作的重要优化方法.
3. Pipeline现实
public void pipeline(){
String key = "pipeline-test";
String old = jedis.get(key);
if(old != null){
System.out.println("Key:" + key + ",old value:" + old);
}
//代码模式1,这种模式是最常见的方式
Pipeline p1 = jedis.pipelined();
p1.incr(key);
System.out.println("Request incr");
p1.incr(key);
System.out.println("Request incr");
//结束pipeline,并开始从相应中获得数据
List<Object> responses = p1.syncAndReturnAll();
if(responses == null || responses.isEmpty()){
throw new RuntimeException("Pipeline error: no response...");
}
for(Object resp : responses){
System.out.println("Response:" + resp.toString());//注意,此处resp的类型为Long
}
//代码模式2
Pipeline p2 = jedis.pipelined();
Response<Long> r1 = p2.incr(key);
try{
r1.get();
}catch(Exception e){
System.out.println("Error,you cant get() before sync,because IO of response hasn't begin..");
}
Response<Long> r2 = p2.incr(key);
p2.sync();
System.out.println("Pipeline,mode 2,--->" + r1.get());
System.out.println("Pipeline,mode 2,--->" + r2.get());
}
Transaction:事务
Redis提供了简单的“事务”能力,MULTI,EXEC,DISCARD,WATCH/UNWATCH指令用来操作事务。
-
MUTIL:开启事务,此后所有的操作将会添加到当前链接的事务“操作队列”中。
-
EXEC:提交事务,EXEC指令将会触发事务中所有的操作被写入AOF文件(如果开启了AOF),然后开始在内存中实施这些数据变更操作;Redis将会尽力确保事务中所有的操作都能够执行,如果redis环境故障,有可能导致事务未能成功执行,那么需要在redis重启后增加额外的校验工作。
-
DISCARD:取消事务,记住,此指令不是严格意义上的“事务回滚”,只是表达了“事务操作被取消”的语义,将会导致事务的操作队列中的操作不会被执行,且事务关闭。
-
WATCH/UNWATCH:“观察”,这个操作也可以说是Redis的特殊功能,但是也可说是Redis不能提供“绝对意义上”的事务能力而增加的一个“补充特性”(比如事务隔离,多事务中操作冲突解决等);在事务开启前,可以对某个KEY注册“WATCH”,如果在事务提交后,将会首先检测“WATCH”列表中的KEY集合是否被其他客户端修改,如果任意一个KEY 被修改,都将会导致事务直接被“DISCARD”;即使事务中没有操作某个WATCH KEY,如果此KEY被外部修改,仍然会导致事务取消。事务执行成功或者被DISCARD,都将会导致WATCH KEY被“UNWATCH”,因此事务之后,你需要重新WATCH。WATCH需要在事务开启之前执行。WATCH所注册的KEY,事实上无论是被其他Client修改还是当前Client修改,如果不重新WATCH,都将无法在事务中正确执行。WATCH指令本身就是为事务而生,你或许不会在其他场景下使用WATCH;
String key = "transaction-key"; jedis.set(key, "20"); jedis.watch(key);//注册key,此后key将会被监控,如果在事务执行前被修改,则导致事务被DISCARD。 jedis.incr(key);//此key被修改,即使是自己,也会导致watch在事务中执行失效 jedis.unwatch();//取消注册 jedis.watch(key);//重新注册,在重新注册前,必须unwatch Transaction tx = jedis.multi();//开启事务 ....
Redis中,如果一个事务被提交,那么事务中的所有操作将会被顺序执行,且在事务执行期间,其他client的操作将会被阻塞;Redis采取了这种简单而“粗鲁”的方式来确保事务的执行更加的快速和更少的外部干扰因素。
如果事务中出现错误:
- 还未exec就报错:如果事务中出现语法错误,则事务会成功回滚,整个事务中的命令都不会提交;
- 成功执行exec后才报错:如果事务中出现的不是语法错误,而是执行错误,不会触发回滚,该事务中仅有该错误命令不会提交,其他命令依旧会继续提交,因此这里的"事务"打了个引号,和我们通常理解的数据库事务完全不一样。
Redis的事务之所以如此设计,它为了确保本身的性能,同时不引入“关系型数据库”的设计复杂度;你不能完全希望Redis能为你交付完美的事务操作,只能说,你选择了错误的工具。
public void transaction(){
String key = "transaction-key";
jedis.set(key, "20");
jedis.watch(key);
Transaction tx = jedis.multi();
tx.incr(key);
tx.incr(key);
tx.incr(key);
List<Object> result = tx.exec();
if(result == null || result.isEmpty()){
System.out.println("Transaction error...");//可能是watch-key被外部修改,或者是数据操作被驳回
return;
}
for(Object rt : result){
System.out.println(rt.toString());
}
}
注意:本文归作者所有,未经作者允许,不得转载