redis作为高速缓存在我们的开发场景中经常应用,Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
Redis支持数据的备份,即master-slave模式的数据备份。
Redis的优势
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
在Java语言中最常用的连接redis服务的客户端包就是jedis,jedis实现对redis操作处理的几个连接类,Jedis、jedisPool、ShardedJedis、ShardedJedisPool,JedisSentinelPool几部分,jedis操作单实例的redis,jedisPool单实例的连接池实现,ShardedJedis操作多节点的redis,即分片存储实现,ShardedJedisPool多实例redis节点分片连接池,JedisSentinelPool哨兵模式连接池,我们通过对这几部分简单应用理解jedis对redis的操作方式。
1,单实例模式:单个redis服务节点,
通过简单对单节点的jedis对象直接操作demo学习jedis,示例代码:
import redis.clients.jedis.Jedis;
public class JedisUtil {
private static Jedis jedis = null;
private final String host;
private final Integer port;
private String password;
public JedisUtil(final String host, final int port) {
this.host = host;
this.port = port;
initJedis();
}
public JedisUtil(final String host, final int port, final String password) {
this.host = host;
this.port = port;
this.password = password;
initJedis();
}
public void initJedis() {
if (jedis == null) {
jedis = new Jedis(host, port);
System.out.println("创建jedis连接成功");
if (password != null) {
jedis.auth(password);
}
}
}
public void set(String key, String value) {
try {
String result = jedis.set(key, value);
System.out.println(result);
if ("OK".equals(result)) {
}
} finally {
jedis.close();
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
JedisUtil jedisUtil = new JedisUtil("127.0.0.1", 4100);
jedisUtil.set("test" + i, "value" + i);
}
}
}
查看jedis类源码,发现jedis继承了BinaryJedis类和实现了很多Command接口,JedisCommands, MultiKeyCommands,AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands。
BinaryJedis类是和redis服务连接的交互,JedisCommands接口定义了通用分片和非分片的set,get等接口,BasicCommands接口定义了ping,quit,flushAll,info等基础命令接口,ClusterCommands接口定义了集群接口,SentinelCommands接口定义哨兵接口
jedis实例化源代码过程
public Jedis(final String host, final int port) {
super(host, port);
}
public BinaryJedis(final String host, final int port) {
client = new Client(host, port);
}
由于jedis继承了BinaryJedis类,在BinaryJedis中实例化了clent对象,client继承BinaryClient,BinaryClient继承了Connection类,
BinayClient中主要是发送命令的操作,connection是跟redis服务交互。
首先看set方法
/**
* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1GB).
* <p>Time complexity: O(1)
* @param key
* @param value
* @return Status code reply
*/
public String set(final String key, String value) {
checkIsInMultiOrPipeline();
client.set(key, value);
return client.getStatusCodeReply();
}
在调用client的set方法之前,调用了checkIsInMultiOrPipeline();来判断是否设定了mult或者Pipeline模式, 查看手册pipeline 只是把多个redis指令一起发出去,redis并没有保证这些指定的执行是原子的;multi相当于一个redis的transaction的,保证整个操作的原子性,避免由于中途出错而导致最后产生的数据不一致。通过测试得知,pipeline方式执行效率要比其他方式高10倍左右的速度,启用multi写入要比没有开启慢一点。
set方法委托client进行处理
@Override
public void set(final String key, final String value) {
set(SafeEncoder.encode(key), SafeEncoder.encode(value));
}
掉用了BinaryClient的set方法
public void set(final byte[] key, final byte[] value) {
sendCommand(Command.SET, key, value);
}
掉用了Connection的sendCommand方法
protected Connection sendCommand(final Command cmd, final byte[]... args) {
try {
connect();
Protocol.sendCommand(outputStream, cmd, args);
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
/*
* When client send request which formed by invalid protocol, Redis send back
* error message before close connection. We try to read it to provide reason of
* failure.
*/
try {
String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
if (errorMessage != null && errorMessage.length() > 0) {
ex = new JedisConnectionException(errorMessage, ex.getCause());
}
} catch (Exception e) {
/*
* Catch any IOException or JedisConnectionException occurred from
* InputStream#read and just ignore. This approach is safe because reading error
* message is optional and connection will eventually be closed.
*/
}
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
1,掉用connection()方法进行连接
public void connect() {
if (!isConnected()) {
try {
socket = new Socket();
// ->@wjw_add
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
// valid
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
// ensure timely delivery of data
socket.setSoLinger(true, 0); // Control calls close () method,
// the underlying socket is closed
// immediately
// <-@wjw_add
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
socket.setSoTimeout(soTimeout);
if (ssl) {
if (null == sslSocketFactory) {
sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
}
socket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, true);
if (null != sslParameters) {
((SSLSocket) socket).setSSLParameters(sslParameters);
}
if ((null != hostnameVerifier) &&
(!hostnameVerifier.verify(host, ((SSLSocket) socket).getSession()))) {
String message = String.format(
"The connection to '%s' failed ssl/tls hostname verification.", host);
throw new JedisConnectionException(message);
}
}
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
}
这里主要使用socket进行通信连接,使用长连接进行通讯减少连接开销,并实例化了RedisOutputStream和RedisInputStream来读取内容
2,掉用protocol的sendCommand方法
public static void sendCommand(final RedisOutputStream os, final Command command,
final byte[]... args) {
sendCommand(os, command.raw, args);
}
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
最后掉用client.getStatusCodeReply();获取返回状态
public String getStatusCodeReply() {
flush();
pipelinedCommands--;
final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
if (null == resp) {
return null;
} else {
return SafeEncoder.encode(resp);
}
}
getStatusCodeReply方法中首先掉用了flush()方法,保证之前发送的命令能发送出去,之后掉用了readProtocolWithCheckingBroken()方法,
protected Object readProtocolWithCheckingBroken() {
try {
return Protocol.read(inputStream);
} catch (JedisConnectionException exc) {
broken = true;
throw exc;
}
}
调用Protocol.read进行对RedisInputStream进行读取,
public static Object read(final RedisInputStream is) {
return process(is);
}
private static Object process(final RedisInputStream is) {
final byte b = is.readByte();
if (b == PLUS_BYTE) {
return processStatusCodeReply(is);
} else if (b == DOLLAR_BYTE) {
return processBulkReply(is);
} else if (b == ASTERISK_BYTE) {
return processMultiBulkReply(is);
} else if (b == COLON_BYTE) {
return processInteger(is);
} else if (b == MINUS_BYTE) {
processError(is);
return null;
} else {
throw new JedisConnectionException("Unknown reply: " + (char) b);
}
}
最后在read的时候对返回的响应进行了判断,枚举出了几种响应方式,对不同的响应进行不同的处理。
整个交互过程是通过Socket方式通信过程,但是这里也有一个问题就是线程安全问题,显然Jedis实例是线程不安全的,对于多线程共享jedis实例是会有问题的。同时直接使用jedis不能避免的需要反复的创建和销毁Socket,开销很大。