多线程jedis的问题分析

共享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系列化协议

转载于:https://my.oschina.net/u/2474629/blog/916684

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值