Jedis 作为 Redis 官方推荐的三个客户端之一,相比于其他两个(Lettuce,Redisson)而言,它提供最基本的客户端功能,也因此使得它具备简单和轻量的特性。此外,它也是线程不安全的客户端,因此不适用在多线程场景中。本篇我们通过对其源码的阅读来了解 Redis 的客户端是如何进行工作的,同时也去找出它线程不安全的原因。
版本说明:本片源码解读基于 Jedis-3.3.0,请注意区分。
首先我们从 Jedis 的基本使用说起,最简单的使用方式如下:
Jedis jedis = new Jedis("localhost", 6379);// 初始化客户端
jedis.auth("foobared");// 密码认证
jedis.dbSize();// 查询字典的数据量
jedis.close();// 关闭连接
了解了基本的用法之后,我们就从客户端的初始化开始,在开始之前不妨先了解一下 Jedis 类的结构:
上面这些带 Binary 的接口或类,其参数均为字节数组,这正是其使用 Binary 修饰的原因。
Jedis 的初始化:
public BinaryJedis(final String host, final int port) {
client = new Client(host, port);
}
public Client(final String host, final int port) {
// 调用父类 BinaryClient 的初始化
super(host, port);
}
public BinaryClient(final String host, final int port) {
// 调用父类 Connection 的初始化
super(host, port);
}
public Connection(final String host, final int port) {
this(host, port, false);
}
public Connection(final String host, final int port, final boolean ssl) {
this(host, port, ssl, null, null, null);
}
public Connection(final String host, final int port, final boolean ssl,
SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
HostnameVerifier hostnameVerifier) {
this(new DefaultJedisSocketFactory(host, port, Protocol.DEFAULT_TIMEOUT,
Protocol.DEFAULT_TIMEOUT, ssl, sslSocketFactory, sslParameters, hostnameVerifier));
}
public Connection(final JedisSocketFactory jedisSocketFactory) {
this.jedisSocketFactory = jedisSocketFactory;
}
DefaultJedisSocketFactory–套接字工厂
public class DefaultJedisSocketFactory implements JedisSocketFactory {
private String host;
private int port;
private int connectionTimeout;
private int soTimeout;
private boolean ssl;
private SSLSocketFactory sslSocketFactory;
private SSLParameters sslParameters;
private HostnameVerifier hostnameVerifier;
public DefaultJedisSocketFactory(String host, int port, int connectionTimeout, int soTimeout,
boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
HostnameVerifier hostnameVerifier) {
this.host = host;
this.port = port;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.ssl = ssl;
this.sslSocketFactory = sslSocketFactory;
this.sslParameters = sslParameters;
this.hostnameVerifier = hostnameVerifier;
}
@Override
public Socket createSocket() throws IOException {
Socket socket = null;
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(getHost(), getPort()), getConnectionTimeout());
socket.setSoTimeout(getSoTimeout());
if (ssl) {
if (null == sslSocketFactory) {
sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
}
socket = sslSocketFactory.createSocket(socket, getHost(), getPort(), true);
if (null != sslParameters) {
((SSLSocket) socket).setSSLParameters(sslParameters);
}
if ((null != hostnameVerifier)
&& (!hostnameVerifier.verify(getHost(), ((SSLSocket) socket).getSession()))) {
String message = String.format(
"The connection to '%s' failed ssl/tls hostname verification.", getHost());
throw new JedisConnectionException(message);
}
}
return socket;
} catch (Exception ex) {
if (socket != null) {
socket.close();
}
throw ex;
}
}
@Override
public String getDescription() {
return host + ":" + port;
}
@Override
public String getHost() {
return host;
}
@Override
public void setHost(String host) {
this.host = host;
}
@Override
public int getPort() {
return port;
}
@Override
public void setPort(int port) {
this.port = port;
}
@Override
public int getConnectionTimeout() {
return connectionTimeout;
}
@Override
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
@Override
public int getSoTimeout() {
return soTimeout;
}
@Override
public void setSoTimeout(int soTimeout) {
this.soTimeout = soTimeout;
}
}
Jedis 中执行命令的过程都实现在 Client 类中,我们以 set 指令为例来进一步讨论:
public String set(final String key, final String value, final SetParams params) {
// 判断当前连接是否处于事务中或者管道中,
// 处于事务中或者管道中,无法使用 Jedis 对象执行任何指令,
// 底层会向上抛出异常
checkIsInMultiOrPipeline();
client.set(key, value, params);
return client.getStatusCodeReply();
}
client.set()
public void set(final String key, final String value, final SetParams params) {
set(SafeEncoder.encode(key), SafeEncoder.encode(value), params);
}
BinaryClient.set()
public void set(final byte[] key, final byte[] value, final SetParams params) {
sendCommand(SET, params.getByteParams(key, value));
}
...
// 发送 Redis 指令的核心方法
public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
// 判断当前是否有可用连接,有则直接使用,否则使用套接字工厂创建
connect();
// 使用套接字,按照 Redis 的序列化协议 RESP 进行字节流的发送
Protocol.sendCommand(outputStream, cmd, args);
} 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;
}
}
...
// 判断当前是否有可用连接,有则直接使用,否则使用套接字工厂创建
public void connect() {
if (!isConnected()) {
try {
socket = jedisSocketFactory.createSocket();
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException("Failed connecting to "
+ jedisSocketFactory.getDescription(), ex);
}
}
}
Protocol.sendCommand()
public static void sendCommand(final RedisOutputStream os, final ProtocolCommand command,
final byte[]... args) {
sendCommand(os, command.getRaw(), 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);
}
}
public String getStatusCodeReply() {
// 刷新输出流
flush();
// readProtocolWithCheckingBroken() 方法继承自 Connection
final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
if (null == resp) {
return null;
} else {
// 将字节流进行编码得到结果
return SafeEncoder.encode(resp);
}
}
readProtocolWithCheckingBroken()
protected Object readProtocolWithCheckingBroken() {
if (broken) {
throw new JedisConnectionException("Attempting to read from a broken connection");
}
try {
return Protocol.read(inputStream);
} catch (JedisConnectionException exc) {
broken = true;
throw exc;
}
}
Protocol.read()
public static Object read(final RedisInputStream is) {
// 按照 RESP 协议进行解析响应
return process(is);
}
...
private static Object process(final RedisInputStream is) {
final byte b = is.readByte();
switch (b) {
case PLUS_BYTE:
return processStatusCodeReply(is);
case DOLLAR_BYTE:
return processBulkReply(is);
case ASTERISK_BYTE:
return processMultiBulkReply(is);
case COLON_BYTE:
return processInteger(is);
case MINUS_BYTE:
processError(is);
return null;
default:
throw new JedisConnectionException("Unknown reply: " + (char) b);
}
}
管道(流水线)的基本使用如下:
Pipeline p = jedis.pipelined();
p.set("foo", "bar");
p.get("foo");
List<Object> results = p.syncAndReturnAll();
System.out.println(results.size());// 2
System.out.println(results.get(0));// "OK"
System.out.println(results.get(1));// "bar"
public Pipeline pipelined() {
pipeline = new Pipeline();
pipeline.setClient(client);
return pipeline;
}
public Response<String> set(final String key, final String value) {
// 通过 client 客户端对象调用操作指令
getClient(key).set(key, value);
// 获取应答信息,BuilderFactory 负责类型转换
return getResponse(BuilderFactory.STRING);
}
getResponse()
protected <T> Response<T> getResponse(Builder<T> builder) {
Response<T> lr = new Response<T>(builder);
pipelinedResponses.add(lr);
return lr;
}
public List<Object> syncAndReturnAll() {
if (getPipelinedResponseLength() > 0) {
// 从连接中获得多个未格式化的响应信息
List<Object> unformatted = client.getMany(getPipelinedResponseLength());
List<Object> formatted = new ArrayList<>();
for (Object o : unformatted) {
try {
formatted.add(generateResponse(o).get());
} catch (JedisDataException e) {
formatted.add(e);
}
}
return formatted;
} else {
return java.util.Collections.<Object> emptyList();
}
}
generateResponse()
protected Response<?> generateResponse(Object data) {
// 指令执行时,向 pipelinedResponses 中添加的 Response 元素
// 依次取出元素,代表已被服务端处理并收到应答
// Response 是对应答信息的封装,根据不同的指令,设置对应的应答数据类型
// 当收到应答后,按照数据类型进行提取转换
Response<?> response = pipelinedResponses.poll();
if (response != null) {
response.set(data);// 设置具体类型的响应值
}
return response;
}
response.get()
public T get() {
// if response has dependency response and dependency is not built,
// build it first and no more!!
if (dependency != null && dependency.set && !dependency.built) {
dependency.build();
}
if (!set) {
throw new JedisDataException(
"Please close pipeline or multi block before calling this method.");
}
if (!built) {
build();
}
if (exception != null) {
throw exception;
}
return response;
}
...
private void build() {
// check build state to prevent recursion
if (building) {
return;
}
building = true;
try {
if (data != null) {
if (data instanceof JedisDataException) {
exception = (JedisDataException) data;
} else {
response = builder.build(data);
}
}
data = null;
} finally {
building = false;
built = true;
}
}
BuilderFactory 工厂类中定义了各种数据类型的建造器内部类,对应答数据进行相应格式的转换。
通过源码的解读,我们了解到,所谓的管道技术就是将多个指令几乎同时发送到服务端,以此来将 N 个 RTT 降低到接近一个 RTT,来提高客户端的吞吐量,效果随着一次发送的指令数的增加而增加。
连接池的基本用法如下:
JedisPool pool = new JedisPool("localhost", 6379, 2000);
Jedis jedis = pool.getResource();
jedis.auth("foobared");
jedis.set("foo", "bar");
System.out.println(jedis.get("foo"));// "bar"
jedis.close();// 将连接归还到连接池
public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
final int connectionTimeout, final int soTimeout, final String password, final int database,
final String clientName) {
// 调用父类 JedisPoolAbstract 的构造器
// GenericObjectPoolConfig 中封装了连接池的属性信息
// JedisFactory 作为工厂,用来创建客户端连接 DefaultPooledObject
super(poolConfig, new JedisFactory(host, port, connectionTimeout, soTimeout, password,
database, clientName));
}
public JedisPoolAbstract(GenericObjectPoolConfig poolConfig, PooledObjectFactory<Jedis> factory) {
// 调用父类 Pool 的构造器
super(poolConfig, factory);
}
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
initPool(poolConfig, factory);
}
...
public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
if (this.internalPool != null) {
try {
// 如果存在则关闭
closeInternalPool();
} catch (Exception e) {
}
}
// 初始化池对象
this.internalPool = new GenericObjectPool<>(factory, poolConfig);
}
public Jedis getResource() {
// 调用父类的方法获取连接
Jedis jedis = super.getResource();
jedis.setDataSource(this);
return jedis;
}
Pool.getResource()
public T getResource() {
try {
// GenericObjectPool 类型的 internalPool 来自 commons-pool 依赖
// 大概逻辑如下:
// 首次获取连接时,使用我们初始化时传入的 JedisFactory 工厂创建一个连接
// 后续如果有空闲的可直接获取到,否则根据配置参数判断能否新建连接
// 如果可以则新建,否则等待直至超时
return internalPool.borrowObject();
} catch (NoSuchElementException nse) {
if (null == nse.getCause()) { // The exception was caused by an exhausted pool
throw new JedisExhaustedPoolException(
"Could not get a resource since the pool is exhausted", nse);
}
// Otherwise, the exception was caused by the implemented activateObject() or ValidateObject()
throw new JedisException("Could not get a resource from the pool", nse);
} catch (Exception e) {
throw new JedisConnectionException("Could not get a resource from the pool", e);
}
}
我们再来观察一下发送指令的核心方法——Connection.sendCommand()
public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
connect();
Protocol.sendCommand(outputStream, cmd, args);
} 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;
}
}
此方法在发送指令前,会先检查连接是否已经创建,如果已经创建则直接使用,否则创建一个新的连接,问题就在这里:
public void connect() {
// 这里的 isConnected() 方法存在并发问题,单多个线程并发时会导致 socket 重复创建
if (!isConnected()) {
try {
socket = jedisSocketFactory.createSocket();
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException("Failed connecting to "
+ jedisSocketFactory.getDescription(), ex);
}
}
}
...
public boolean isConnected() {
return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
&& !socket.isInputShutdown() && !socket.isOutputShutdown();
}