Redis性能提升-Pipeline和Batch操作

一.Redis如何处理命令?
  Redis是一个c/s模式的TCP Server,本身是基于TCP协议的一个Request/Response protocol模式,使用和HTTP类似的请求响应协议.在OSI七层协议(七层结构:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)模型中,TCP属于传输层,HTTP属于应用层,越接近底层的协议一般来说传输速度会更快,所以Redis才有高速缓存,高效的读写功能.
  Redis Client和Redis Server之间使用TCP进行连接,一个Client可以通过一个Socket连接发起多个请求命令,每个请求命令发出后Client通常会阻塞等待Redis服务处理,Redis Server处理完后会将结果以响应报文返回给Client,因此当执行多条命令的时候,如果命令的发送是一条一条的发送,那么每一条命令的执行都需要等待上一条命令执行成功.如:get ‘0’,get ‘1’,get ‘2’,执行过程如下图:
这里写图片描述
  而基本上每个命令都需要两条报文才能完成,再加上网络通信的延迟是不可避免的,假如Client和Server之间的包传输时间是0.125s,执行类似上面的四个命令八个报文就需要至少1s的时间,这样即使Redis每秒能处理100个命令,client也只能1s发送四条命令,这显然没有充分利用redis的处理能力.

二.如何提高redis的处理速度?
场景:记录某一个商品的用户购买群
  这里我们用到了Redis中的Set数据类型,key为productId,value为customerId集合,后续就可以根据productId查看当前customerId是否有购买过该商品,如果是,就可以进行相关推荐.

伪代码如下:

String productID = String.format("productID_{0}", 1);
for (int i = 0; i < 10; i++)
{
	int customerID = i;
	redis.SetAdd(productID, customerID);
}

1.使用Batch操作
利用Batch(mget,mset之类的单条命令)处理多个key的命令,一次性将多个命令提交过去,极大的较少了在网络传输方面带来的损耗.

伪代码:

String productID = String.format("productID_{0}", 1);
List<Integer> list = new List<>();
for (int i = 0; i < 10; i++)
{
	list.Add(i);
}
redis.SetAdd(productID,list.toArray(new int[list.size()]));

2.使用PipeLine管道
  Pipeline可以从client打包多条命令后一起发出,redis服务端会在处理完多条命令后将多条命令的处理结果打包到一起返回给客户端,与单条命令顺序执行相比,使用Pipeline极大的减少了客户端与redis server的通信次数,从而降低往返延时的时间.其过程如下图所示:
这里写图片描述

疑问:使用pipeline一次性打包的命令越多越好吗?
  假设不考虑tcp报文过长被拆分的情况,client可以将多个命令放到一个tcp报文一起发送,server可以将多个命令的处理结果汇聚后放到一个tcp报文返回.但是值得注意的是,用pipeline方式打包命令发送,redis server必须在处理完所有命令前先将所有已处理完的命令的结果缓存起来,打包的命令越多,缓存消耗的内存就越多.所以并是不是打包的命令越多越好。具体多少合适需要根据具体情况测试.

测试代码:
分别向redis中插入100、500、1000、2000、5000、10000数据

long start=System.currentTimeMillis();  
for (int i = 0; i <50000; i++) {  
  redis.set(String.valueOf(i),String.valueOf(i));  
}  
long end=System.currentTimeMillis();  
logger.info("the total time is:"+(end-start));  

Pipeline pipe=redis.pipelined();  
long start_pipe=System.currentTimeMillis();  
for (int i = 0; i <50000; i++) {  
  pipe.set(String.valueOf(i),String.valueOf(i));  
}  
pipe.sync();  
long end_pipe=System.currentTimeMillis();  
logger.info("the pipe total time is:"+(end_pipe-start_pipe));  

测试结果对比:

这里写图片描述

应用场景示例:
  在上面的场景中,我们只是解决了product维度的数据存储,但还有一个customer的维度,即需要维护一个customerId为key的set集合,其中value的值为该customerid的各种平均值,比如说“总交易次数”,“总交易金额”。。。等等这样的聚合信息,然后推送过来的是批量的customerid,也就是说你需要定时维护一小嘬set集合,在这种情况下某一个set的批量操作就搞不定了。。。

原始伪代码如下:

//批量过来的数据:customeridlist,ordertotalprice,具体业务逻辑省略
String orderTotalPrice = 100;
String customerIDList = new List<int>();
for (int i = 0; i < 10; i++){
	 customerIDList.Add(i);
}

//foreach更新每个redis 的set集合
foreach (String item in customerIDList){
  String customerID = String.format("customerid_{0}", item);
  redis.SetAdd(customerID, orderTotalPrice);
}

解决方法:使用pipeline将命令集整合起来通过一条request请求一起送过去,由redis内部fake出一个client做批量执行操作.伪代码:

//批量过来的数据:customeridlist,ordertotalprice,具体业务逻辑省略
Pipeline pipe=redis.pipelined();
String orderTotalPrice = 100;
String customerIDList = new List<int>();
for (int i = 0; i < 10; i++){
	 customerIDList.Add(i);
}

//foreach更新每个redis 的set集合
foreach (String item in customerIDList){
  String customerID = String.format("customerid_{0}", item);
  pipe.set(customerID, orderTotalPrice);  
}
pipe.sync();  

补充:关于pipeline方式背后的一些思考.
redis的pipeline方式的实现原理是先把指令发送服务器端,让redis编译(发送到服务器并编译,这是猜测,因为如果不这样做,pipeline方式不应该有那么大的性能提升。影响一条指令执行时间的因素有网络、编译、执行,后两个是在服务器端,第一个因素是可以忽略的,所以如果指令不是预先发送到服务器端并编译,性能不会有那么大的提升。),但是此时不执行,当客户端调用执行接口的时候才执行相应指令。
具体步骤如下:
1、客户端发送指令
2、服务器端编译并缓存
3、当指令达到一定数量后,客户端开始执行。(这个数值是由客户端应用程序控制的)
非内存数据库提供了一个类似的功能,不同的是,redis的pipeline方式是可以缓存所有类型的指令,非内存数据库是针对某一类指令进行数据填充,比如 insert a to b,非内存数据库会允许预先把insert to 这样的指令模板送到服务器端并编译,然后客户端缓存数据,当达到一定量之后,客户端发送所有的缓存数据到服务器端,服务器端将指令结果返回。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值