Java中多线程服务中遇到的Redis并发问题?

背景:

一个中小型 H5 游戏

 

核心错误信息:

 

  (1): java.lang.ClassCastException: [B cannot be cast to java.lang.Long

  at redis.clients.jedis.Connection.getIntegerReply(Connection.java:201)
  at redis.clients.jedis.Jedis.del(Jedis.java:129) 

 

  (2):redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Socket closed

  at redis.clients.jedis.Protocol.process(Protocol.java:131)
  at redis.clients.jedis.Protocol.read(Protocol.java:187)
  at redis.clients.jedis.Connection.getIntegerReply(Connection.java:201)
  at redis.clients.jedis.Jedis.del(Jedis.java:129)

 

贴上核心问题代码 (Jedis 工具类):

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
     * 获取连接池.
     *
     * @return 连接池实例
     */
    private static JedisPool getPool() {
        String key = ip + ":" + port;
        JedisPool pool = null;
        //这里为了提供大多数情况下线程池Map里面已经有对应ip的线程池直接返回,提高效率
        if (maps.containsKey(key)) {
            pool = maps.get(key);
            return pool;
        }
        //这里的同步代码块防止多个线程同时产生多个相同的ip线程池
        synchronized (JedisUtil.class) {
            if (!maps.containsKey(key)) {
                JedisPoolConfig config = new JedisPoolConfig();
                config.setMaxTotal(maxTotal);
                config.setMaxIdle(maxIdle);
                config.setTestOnBorrow(true);
                config.setTestOnReturn(true);
                config.setMaxWaitMillis(maxWaitMillis);
                config.setBlockWhenExhausted(blockWhenExhausted);
                try {
                    if (password != null && !"".equals(password)) {
                        pool = new JedisPool(config, ip, port, timeout, password);
                    } else {
                        pool = new JedisPool(config, ip, port, timeout);
                    }
                    maps.put(key, pool);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                pool = maps.get(key);
            }
        }
        return pool;
    }
 
    /**
     * 获取Redis实例.
     *
     * @return Redis工具类实例
     */
    public Jedis getJedis() {
        Jedis jedis = null;
        int count = 0;
        while (jedis == null && count < retryNum) {
            try {
                JedisPool pool = getPool();
                jedis = pool.getResource();
            } catch (Exception e) {
                logger.error("get redis master failed!", e);
            } finally {
                closeJedis(jedis);
            }
            count++;
        }
        return jedis;
    }
 
    /**
     * 释放redis实例到连接池.
     *
     * @param jedis redis实例
     */
    public void closeJedis(Jedis jedis) {
        if (jedis != null) {
            getPool().returnResource(jedis);
        }
    }

  

问题剖析:

  查阅了线上资料, 发现是由于多线程使用了同一个 Jedis 实例导致的并发问题.

 

结果:

  一开始, 我发现我调用了getJedis()获取了 jedis 实例并使用后没有关闭.

  于是我把关闭 Jedis 的操作加上去了

  结果是错误的量少了

  但还是有报错,说明这是其中一个问题.

  最后还是没能使用 Jedis 连接池搞定这个问题

 

解决办法:

  抛弃使用连接池

  每次使用 Jedis 都生成一个独立的实例

  每次用完以后就 close()

  这样也就不存在并发的问题了

  这样做有一个潜在的问题是如果并发量达到很大值,Redis 连接数被塞满的话还是会出现问题.

  一般情况下不是非常大的并发, 用完就 close 的话, 没那么容易到这个瓶颈

 

相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 获取一个独立的Jedis实例
 * @return jedis
 */
public Jedis getSingleJedis() {
    Jedis jedis = new Jedis(ip, port, timeout);
    jedis.connect();
    if (StringUtils.isNotBlank(password)) {
        jedis.auth(password);
    }
    return jedis;
}<br><br>  // 关闭Jedis直接调用 jedis.close() 即可