共享jedis实例引起的socket异常
redis的安装和启动这里就不介绍了啊,直接看例子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
public class BadConcurrentJedisTest {
private static final ExecutorService pool = Executors.newFixedThreadPool(20);
private static final Jedis jedis = new Jedis("localhost");
public static void main(String[] args) {
for(int i=0;i<20;i++){
pool.execute(new RedisSet());
}
}
static class RedisSet implements Runnable{
@Override
public void run() {
while(true){
jedis.set("hello", "world");
}
}
}
}
看上面的例子开20个线程来执行set hello world,会出现2个主要的错误:
java.net.SocketException: Socket closed
java.net.SocketException: Socket is not connected
为什么会出现这2个错误呢? 我们可以很容易的通过堆栈信息定位到redis.clients.jedis.Connection的connect方法:
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);
}
}
}
jedis在执行每一个命令之前都会先执行connect方法,socket是一个共享变量,在多线程的情况下可能存在:线程1执行到了
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
线程2执行到下面的语句之间:
socket = new Socket();
线程2
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
因为线程2重新初始化了socket但是还没有执行connect,所以线程1执行socket.getOutputStream()或者socket.getInputStream()就会抛出java.net.SocketException: Socket is not connected。
java.net.SocketException: Socket closed是因为socket异常导致共享变量socket关闭了,引起的。
下面是测试socket的一下状态,注意连接的是redis服务器,或者其他监听6379的服务器要启动。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.junit.Test;
public class SocketTest {
static Socket socket;
private static String host = "localhost";
private static int port = 6379;
@Test
public void testSocketStatus() {
socket = new Socket();
try {
socket.connect(new InetSocketAddress(host, port), 3000);
// socket.connect(new InetSocketAddress(host, port), 3000);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(socket.isBound());
System.out.println(socket.isClosed());
System.out.println(socket.isConnected());
System.out.println(socket.isInputShutdown());
System.out.println(socket.isOutputShutdown());
}
}
如果不执行:
socket.connect(new InetSocketAddress(host, port), 3000);
则都为false,如果执行2次上面的代码就会抛出java.net.SocketException: Socket is not connected异常。
上面是因为多个线程共享jedis引起的socket异常。除了socket连接引起的异常之外,还有就是共享数据流引起的异常。下面就看一下,因为共享jedis实例引起的共享数据流错误问题。
共享jedis引起的共享数据流错误
为了避免多线程连接的时候引起的错误,我们在初始化的时候就先执行一下connect操作
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
public class BadConcurrentJedisTest {
private static final ExecutorService pool = Executors.newCachedThreadPool();
private static final Jedis jedis = new Jedis("localhost");
static{
jedis.connect();
}
public static void main(String[] args) {
for(int i=0;i<20;i++){
pool.execute(new RedisTest());
}
}
static class RedisTest implements Runnable{
@Override
public void run() {
while(true){
jedis.set("hello", "world");
}
}
}
}
上面的代码可能会存在下面的错误:
redis.clients.jedis.exceptions.JedisConnectionException: Unknown reply: E
redis.clients.jedis.exceptions.JedisDataException: RR Protocol error: expected '$', got ' '
redis.clients.jedis.exceptions.JedisDataException: RR Protocol error: invalid multibulk length
java.net.SocketException: Connection reset
前面三个是因为多线程通过RedisInputStream和RedisOutputStream读写缓冲区的时候引起的问题造成的数据问题不满足RESP协议引起的。RESP协议可以参考 Redis序列化协议
几个简单的例子,例如多个线程执行命令,线程1执行 set hello world命令。本来应该发送:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
但是线程执行写到
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n
然后被挂起了,线程2执行了写操作写入了' ',然后线程1继续执行,最后发送到redis服务器端的数据可能就是:
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n' '$5\r\nworld\r\n
这种情况就会出现:
redis.clients.jedis.exceptions.JedisDataException: RR Protocol error: expected '$', got ' '
这样的错误。
至于java.net.SocketException: Connection reset错误,是因为redis服务器接受到错误的命令,执行了socket.close这样的操作,关闭了连接。服务器会返回复位标志"RST",但是客户端还在继续执行读写数据操作。
jedis多线程操作
jedis本身不是多线程安全的,这不是jedis的bug,而是jedis的设计与redis本身就是单线程相关。看jedis实例抽象的都是发送命令相关的,所以一个jedis实例使用1个线程和100个线程没有什么本质的区别,所以并不需要设计为线程安全的。
那么问题来了要多线程方式访问redis服务器怎么办呢?答案就是使用多个jedis实例,每一个线程一个jedis实例,而不是一个jedis实例多个线程共享。一个jedis关联一个Client相当于一个客户端,Client继承了Connection,Connection维护了Socket连接,对于Socket这种昂贵的连接,一般都会做池化的,jedis提供了JedisPool。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class JedisPoolTest {
private static final ExecutorService pool = Executors.newCachedThreadPool();
private static final CountDownLatch latch = new CountDownLatch(20);
private static final JedisPool jPool = new JedisPool();
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(int i=0;i<20;i++){
pool.execute(new RedisTest());
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() - start);
pool.shutdownNow();
}
static class RedisTest implements Runnable{
@Override
public void run() {
Jedis jedis = jPool.getResource();
int i = 10000;
try{
while(i-->0){
jedis.set("hello", "world");
}
}finally{
jedis.close();
latch.countDown();
}
}
}
}
##参考 redis系列化协议