前言
官网推荐的Java客户端有3个,地址:redis.io/clients ,标黄色五角星的是推荐的。
Spring 操作 Redis 提供了一个模板方法 RedisTemplate,这个是不是Spring官方开发的一个客户端呢?其实不是,Spring定义了一个连接工厂接口,RedisConnectionFactory,这个接口有很多实现,例如:JedisConnectionFactory、JredisConnectionFactory、LettuceConnectionFactory、SrpConnectionFactory。
也就是说RedisTemplate对其他现成的客户端再进行了一层封装而已,在Spring Boot2.x版本之前,RedisTemplate默认使用Jedis,2.x版本之后默认使用Lettuce。所以我们学习的底层最终还是这三个客户端
本文源码在:github.com/xuhaoj/redi…
Jedis
基本用法
官网地址,github.com/redis/jedis ,Jedis是我们最熟悉和最常用的客户端,如果不用RedisTemplate 的话可以直接创建 Jedis 的连接。
public static void main(String[] args) {
Jedis jedis = new Jedis("39.103.144.86", 6379);
//如果设置密码的话
//jedis.auth("xushuaige");
jedis.set("jackxu", "shuaige");
System.out.println(jedis.get("jackxu"));
jedis.close();
}
源码在com/xhj/jedis/client/BasicTest.java
连接池
但是使用 Jedis 有一个问题,多个线程使用一个连接的时候线程是不安全的。 这时候有个解决办法就是使用连接池
,为每个请求创建不同的连接,基于Apache common pool实现。
Jedis 的连接池有三个实现:JedisPool、ShardedJedisPool、JedisSentinelPool,都是用 getResource 从连接池获取一个连接。
/**
* 普通连接池
*/
public static void ordinaryPool() {
JedisPool pool = new JedisPool("39.103.144.86", 6379);
Jedis jedis = pool.getResource();
jedis.set("jackxu", "帅哥");
System.out.println(jedis.get("jackxu"));
}
/**
* 分片连接池
*/
public static void shardedPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("192.168.44.181", 6379);
// 连接池
List<JedisShardInfo> infoList = Arrays.asList(shardInfo1);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = jedisPool.getResource();
jedis.set("jackxu", "分片测试");
System.out.println(jedis.get("jackxu"));
}
/**
* 哨兵连接池
*/
public static void sentinelPool() {
String masterName = "redis-master";
Set<String> sentinels = new HashSet<String>();
sentinels.add("192.168.44.186:26379");
sentinels.add("192.168.44.187:26379");
sentinels.add("192.168.44.188:26379");
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
pool.getResource().set("jackxu", "哨兵" + System.currentTimeMillis() + "连接池");
System.out.println(pool.getResource().get("jackxu"));
}
Jedis 的功能比较完善,Redis官方的特性全部支持,比如发布订阅、事务、Lua脚本、客户端分片、哨兵、集群、pipeline等等。
Jedis 连接 Sentinel 需要配置所有的哨兵地址,Cluster 连接哨兵只需要配置任何一个 master 或者 slave的地址就可以了。
分布式锁
用Jedis实现分布式锁,大家可以看下 《介绍几种常见的分布式锁写法》,这篇文章也是我写的。
Pipeline
我们平时说 Redis 是单线程的,说的是 Redis 的请求是单线程处理的,只有上一个命令的相应结束以后,下一个命令才会去处理。如果一次要操作10万个Key,客户端跟服务端就要交互10万次,排队的时间加上网络通信的时间,就会非常的慢。
怎么解决这个问题呢?跟Kafka、MySQL类似,把一组命令组装在一起然后发给Redis的服务端执行,然后一次性获得返回的结果,这个就是pipeline。pipeline通过一个队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器。
下面我们做实验来测试一下,使用 pipeline 的方式在 get 和 set 的时候都要使用 pipeline 的写法。先写set 的对比:
public static void main(String[] args) {
Jedis jedis = new Jedis("39.103.144.86", 6379);
long t1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
jedis.set("commonBatch" + i, "" + i);
}
long t2 = System.currentTimeMillis();
System.out.println("普通set耗时:" + (t2 - t1) + "ms");
Pipeline pipelined = jedis.pipelined();
long t3 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
pipelined.set("pipelineBatch" + i, "" + i);
}
pipelined.syncAndReturnAll();
long t4 = System.currentTimeMillis();
System.out.println("pipeline set耗时:" + (t4 - t3) + "ms");
}
普通的 set 我等了几分钟都没好,实在等不及就关掉了,感兴趣的小伙伴可以试下到底用多长时间,而 pipeline 的方式只用了4秒!
接下来测试 get 的速度
public static void main(String[] args) {
new Thread(() -> {
Jedis jedis = new Jedis("39.103.144.86", 6379);
Set<String> keys = jedis.keys("pipelineBatch*");
List<String> result = new ArrayList();
long t1 = System.currentTimeMillis();
for (String key : keys) {
result.add(jedis.get(key));
}
for (String src : result) {
//System.out.println(src);
}
System.out.println("直接get耗时:" + (System.currentTimeMillis() - t1));
}).start();
new Thread(() -> {
Jedis jedis = new Jedis("39.103.144.86", 6379);
Set<String> keys = jedis.keys("pipelineBatch*");
List<Object> result = new ArrayList();
Pipeline pipelined = jedis.pipelined();
long t1 = System.currentTimeMillis();
for (String key : keys) {
pipelined.get(key);
}
result = pipelined.syncAndReturnAll();
for (Object src : result) {
//System.out.println(src);
}
System.out.println("Pipeline get耗时:" + (System.currentTimeMillis() - t1));
}).start();
}
普通的 get 依然卡主了,而 pipeline 方式的 get 仅用了14秒。
jedis-pipeline 的 client-buffer 的限制是 8192bytes,客户端堆积的命令超过8M时,会发送给服务端。pipeline 对于条数没有限制,但是命令可能会受限于 TCP 包的大小。如果某些操作需要马上得到 redis 操作结果的这种场景不适合用 pipeline,对于实时性和成功性要求不高的可以用 pipeline。
源码在com/xhj/jedis/pipeline/PipelineGet.java、com/xhj/jedis/pipeline/PipelineSet.java
Lettuce
官网地址:lettuce.io ,lettuce是生菜、莴苣的意思,与 Jedis 相比,Lettuce 则完全克服了线程不安全的缺点。Lettuces 是一个可伸缩的线程安全的Redis客户端,支持同步、异步和响应式模式(Reactive)。
Lettuce基于 Netty 框架构建,支持 Redis 的全部高级功能,如发布订阅、事务、Lua脚本、 Sentinel、集群、Pipeline支持连接池。
同步调用
public static void main(String[] args) {
// 创建客户端
RedisClient client = RedisClient.create("redis://39.103.144.86:6379");
// 线程安全的长连接,连接丢失时会自动重连
StatefulRedisConnection<String, String> connection = client.connect();
// 获取同步执行命令,默认超时时间为 60s
RedisCommands<String, String> sync = connection.sync();
// 发送get请求,获取值
sync.set("jackxu", "shuaige");
String value = sync.get("jackxu");
System.out.println(value);
//关闭连接
connection.close();
//关掉客户端
client.shutdown();
}
代码在com/xhj/lettuce/LettuceSyncTest.java
异步调用
异步的结果使用 RedisFuture 包装,提供了大量的回调的方法
public static void main(String[] args) {
RedisClient client = RedisClient.create("redis://39.103.144.86:6379");
// 线程安全的长连接,连接丢失时会自动重连
StatefulRedisConnection<String, String> connection = client.connect();
// 获取异步执行命令api
RedisAsyncCommands<String, String> commands = connection.async();
// 获取RedisFuture<T>
commands.set("jackxu", "shuaige");
RedisFuture<String> future = commands.get("jackxu");
try {
String value = future.get(60, TimeUnit.SECONDS);
System.out.println(value);
} catch (Exception e) {
e.printStackTrace();
}
}
代码在com/xhj/lettuce/LettuceASyncTest.java
RedisTemplate
开头的时候介绍了 Lettuce 是 Spring Boot2.x 的默认客户端,替换了 Jedis。集成之后我们不需要单独使用它,直接调用 Spring 的 RedisTemplate 操作即可,连接的创建和关闭也不需要我们关心。
下面我们看下怎么使用,先配置下RedisTemplate。
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
使用的时候注入下,get set用法很简单。
@Resource
private RedisTemplate<String, Object> redisTemplate;
//set
redisTemplate.opsForValue().set(key, value);
//get
redisTemplate.opsForValue().get(key);
代码在com/xhj/lettuce/RedisConfig.java、com/xhj/lettuce/RedisUtil.java
Redisson
官网:redisson.org ,github.com/redisson/re…
介绍
Redisson 是一个在 Redis 的基础上实现的 Java 主内存数据网络(In-Memory Data Grid),提供了分布式和可扩展的Java数据结构,比如分布式的 Map、List、Queue、Set,不需要自己去运行一个服务实现。
基于 Netty 实现,采用非阻塞 IO,性能高;支持异步请求。支持连接池、pipeline、LUA Scripting、Redis Sentinel、Redis Cluster;不支持事务,官方建议以 LUA Scripting 代替事务;主从、哨兵、集群都支持。Spring 也可以配置和注入 RedissonClient。
分布式锁
还是见《介绍几种常见的分布式锁写法》 这篇文章。
Redisson 跟 Jedis 的定位不同,它不是一个单纯的 Redis 客户端,而是基于 Redis 实现的分布式服务,可以实现一些分布式的数据结构。
手写客户端
前面介绍了三种常见的、热门的客户端,我们有一个想法,能不能自己手写一个客户端呢?答案是可以的。
首先我们知道 Redis 默认监听 6379 的端口号,可以通过 TCP 的方式建立连接,在 Java 中我们可以通过Socket 的方式来进行 TCP 间的通信。其次是与 Redis 服务端之间进行通信使用的是 RESP 协议(Redis Serialization Protocol),这是一种特殊的消息格式,每个命令都是以\r\n(回车+换行)结尾。
发消息或者响应消息需要按这种格式编码,接收消息需要按这种格式解码。这种编码格式在AOF文件中也是这么做的。下面我们通过抓包工具来具体看下这个协议长什么样。
抓包
首先用抓包工具过滤条件ip.dst==39.103.144.86 and tcp.port in {6379}
,然后用 Jedis 执行一下get set的命令
执行一下,打开抓包工具
我们看下set命令
实际发出的数据包是
这里的*3代表三个参数 set jackxu shuaige,$3代表set是三个字符,$6代表jackxu是六个字符,$7代表shuaige是七个字符
*3\r\n$3\r\nSET\r\n$6\r\njackxu\r\n$7\r\nshuaige
再来看下get命令
实际发出的数据包是
这里的*2代表三个参数 get jackxu,$3代表get是三个字符,$6代表jackxu是六个字符
*2\r\n$3\r\nGET\r\n$6\r\njackxu
其实就是把命令、参数和它们的长度用\r\n连接起来。
手写客户端
熟悉完上面的原理后,我们就可以开始手写客户端了。
- 建立Socket连接
- OutputStream写入数据(发送命令到服务端)
- InputStream读取数据(从服务端接收数据)
/**
* @author jackxu
*/
public class MyClient {
private Socket socket;
private OutputStream write;
private InputStream read;
public MyClient(String host, int port) throws IOException {
socket = new Socket(host, port);
write = socket.getOutputStream();
read = socket.getInputStream();
}
/**
* 实现了set方法
*
* @param key
* @param val
* @throws IOException
*/
public void set(String key, String val) throws IOException {
StringBuffer sb = new StringBuffer();
// 代表3个参数(set key value)
sb.append("*3").append("\r\n");
// 第一个参数(set)的长度
sb.append("$3").append("\r\n");
// 第一个参数的内容
sb.append("SET").append("\r\n");
// 第二个参数key的长度(不定,动态获取)
sb.append("$").append(key.getBytes().length).append("\r\n");
// 第二个参数key的内容
sb.append(key).append("\r\n");
// 第三个参数value的长度(不定,动态获取)
sb.append("$").append(val.getBytes().length).append("\r\n");
// 第三个参数value的内容
sb.append(val).append("\r\n");
// 发送命令
write.write(sb.toString().getBytes());
byte[] bytes = new byte[1024];
// 接收响应
read.read(bytes);
System.out.println("set response:" + new String(bytes));
}
/**
* 实现了get方法
*
* @param key
* @throws IOException
*/
public void get(String key) throws IOException {
StringBuffer sb = new StringBuffer();
// 代表2个参数
sb.append("*2").append("\r\n");
// 第一个参数(get)的长度
sb.append("$3").append("\r\n");
// 第一个参数的内容
sb.append("GET").append("\r\n");
// 第二个参数key的长度
sb.append("$").append(key.getBytes().length).append("\r\n");
// 第二个参数内容
sb.append(key).append("\r\n");
write.write(sb.toString().getBytes());
byte[] bytes = new byte[1024];
read.read(bytes);
System.out.println("get response:" + new String(bytes));
}
public static void main(String[] args) throws IOException {
MyClient client = new MyClient("39.103.144.86", 6379);
client.set("myclient", "666");
client.get("myclient");
}
}
代码在com/xhj/myclient/MyClient.java
执行一下,成功 set 和 get 到值了!
结尾
最后的手写一个客户端相当于是底层原理,知识扩展,就像我们手写一个tomcat,手写一个RPC框架,都是一个简易版的东西,便于我们的理解,但是不能用到生产哦。
本文介绍的三种客户端有的介绍比较详细,有的介绍比较简单,具体选型和具体场景大家在使用过程中还需要去查找官网,本文只能说是一个初级入门科普贴,最后感谢大家的观看!