基于Netty的Redis客户端-Nedis

最近温习了一遍Redis命令,忧伤的是很多东西已交还给老师,正好赶上antirez大神在愚人节发布了Redis 3.0,Redis终于有了支持集群的正式版本,于是心血来潮决定自己实现一个Redis客户端来抚慰我这颗忧伤的心灵。

Jedis已经足够强大,它的网络连接是基于阻塞式IO,实现非常简单易懂,但是OIO和NIO相比性能上有劣势,于是决定通过NIO来实现和Redis服务器的网络连接,现在业界最优秀的NIO框架非Netty莫属了,正好以前也学过Netty框架,所以决定基于Netty来实现这个Redis客户端,这样还可以同时再次熟悉一下Netty,于是一个高大上的名字新鲜出炉-Nedis。关于命令的实现就没什么好纠结,完全参照Redis官方文档来就可以了,也可以参考Jedis代码。

由于本码农平时工作比较忙,在公司工作时是不可能抽时间来搞的,一没时间,大佬们各种催活,二是由于公司的信息安全政策,公司里面写的代码是拿不出来的。所以只有利用晚上下班时间和周末的业余时间来搞,工作日有2-3个小时的时间,大概10点开搞,到1点左右,周末由于要带娃做饭,也只能挤出3-4个小时出来,所以进展比较慢,从4月初到现在将近20天的时间终于完成了key、string、hash、list、set、 SortedSet的所有单机命令以及客户端分片(Sharding),其它的事务、lua脚本、集群等功能还未实现,留到后面版本再实现。

框架组件

首先最优先的是要确定代码的基本骨架,骨架确定之后各个命令的实现就纯粹是工作量上的事了,事实上这种工作非常的机械。经过多番的修改重构最终确定了代码骨架,命令的基本流程,下面是Nedis中几个基础的组件:

  • NedisClient和ShardedNedis:该框架中的两个门面,分别处理单机命令和客户端key分片,对使用提供了所有命令的调用接口,由于事务和集群等功能还没有提供,所有未来可能会增加TransationNedis和ClusterNedis等。所有命令最终都会经过Nedis转发,SharedNedis最终也会调用NedisClient。NedisClient的构建采用build模式,通过NedisClientBuilder来构建,关于客户端参数,目前设计得比较粗糙,参数较少,目前可以给NedisClient设置如下参数:

参数 左右
connectTimeoutMills 连接超时时间
eventLoopGroupSize Netty框架的线程池大小
tcpNoDelay 是否设置TCP_NO_DELAY标识
connectionPoolSize 连接池大小
maxConnectionIdleTimeInMills 空闲连接超时时间
minIdleConnections 最小空闲连接数量

  • ConnectionPool:为了减少重复建连开销,采用连接池复用连接,命令最终通过连接池中的连接发送,通过IdleStateHandler监控空闲的连接。
  • ProtocolDecoder:读取命令响应,对响应进行解析,继承自ReplayingDecoder处理分包传输,命令响应的解析最终委托给RedisProtocol进行处理。
  • ResponseReceiver:接收ProtocolDecoder的响应解析结果,把结果交给ResponeAdapter进行适配,并把最终适配后的结果通过ResponseCallback回调返回给命令调用者,处理完成之后最终把连接返回到连接池。
  • ResponeAdapter:把ProtocolDecoder解析出来的结果适配成对使用者比较友好的类型。
  • ResponseCallback:因为Netty是一个异步处理框架,所以我们提供的命令接口不会阻塞直到命令返回(这样就无法体现NIO和Netty的优势了),而是在命令响应达到时通过ResponseCallback回调通知调用者。

下面是命令发送和响应的时序:

技术分享

测试

就目前已完成的代码来讲,Nedis中的单测代码远远超出了框架代码,目前的单测代码8000行左右而框架代码只有4000左右,而且最关键的是由于时间有限,测试代码并没有覆盖所有路径,有些命令特别是分片相关的命令接口还没写单测(那些没完成的单元测试代码只有等后面慢慢补上),所以覆盖率估计50%都不到,也就是说正常的比例单元测试代码:框架代码应该大于4:1,是不是很震惊,但是测试代码是必须要写的,这是一件一劳永逸的事儿,有了单测代码后面改代码时很容易验证修改的代码有没有问题会不会引发其它问题,当然前提是测试代码要可靠。

对我们的命令测试,有两个问题:

  • 在测试命令时可能要依赖其它命令提供数据,比如我想测一个get命令,那么在之前需要通过set命令构造数据来支持get命令的测试,这就要求在get命令执行之前必须得先执行set命令,但是由于Netty框架通过线程池来执行任务,set命令和get命令可能会由不同的线程来执行,这样的话命令执行可能会乱序,即使set命令再get命令之前调用,也不能完全保证set命令先到达服务器,所有在调用set命令接口需要做一个小停顿再调用get命令接口,来保证set命令在get命令之前执行,这个停顿时间一般100ms就够了(我设的是200ms)。
  • 因为Netty框架是异步的,所有调用命令接口时不会发生阻塞,所以为了验证测试效果,需要保证单测方法在响应回来之前不会结束,我们通过一个同步控制器CountDownLatch,在方法结束前调用await方法进行阻塞,在最后一个响应回来时调用countDown释放,嗯,灰常不优雅。

下面是一个测试方法,controller就是上面说的CountDownLatch,CMD_PAUSE_TIME是停顿时间,它是一个放在基类中的常量,可以随便修改,修改之后所有的测试方法都会生效:

@Test
public void testSet() {

	doCmdTest(new TestAction() {

		@Override
		public void doTest() throws InterruptedException, NedisException {
			client.flushAll(null);
			Thread.sleep(CMD_PAUSE_TIME);
			client.set(new ResponseCallback<String>() {

				@Override
				public void done(String result) {
					assertEquals("OK", result);
				}

				@Override
				public void failed(Throwable cause) {
					fail(cause);
				}
			}, "key1", "value1");

			Thread.sleep(CMD_PAUSE_TIME);
			client.set(new ResponseCallback<String>() {

				@Override
				public void done(String result) {
					assertEquals("OK", result);
					controller.countDown();
				}

				@Override
				public void failed(Throwable cause) {
					fail(cause);
					controller.countDown();
				}
			}, "key1", "value2");

		}
	});

}

分享

本着人人为我我为人人,框架代码已经上传到github:https://github.com/chenyihan/nedis,代码需要有JDK7进行编译,接下来还会实现剩余的功能。非常惭愧从github上搞到过很多宝贵的资源,但是迄今为止只共享过两份代码,以后一定要为github多多贡献,大家共同进步。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。