V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Buffer2Disk
V2EX  ›  程序员

多线程操作 redis 会有并发问题嘛?

  •  
  •   Buffer2Disk · 2019-08-01 15:42:20 +08:00 · 6649 次点击
    这是一个创建于 1941 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题, 我看网上都说 redis 是单线程的,内部使用 epoll

    我这边使用 jedis 来操作 redis, jedis 从 jedisPool 中取出来

    有两个线程 1,2

    线程 1 不断的 get ( keyA )

    线程 2 不断的 lpop (keyB)

    keyA 和 keyB 是 2 个不同的键

    现在我发现在并发操作的时候,lpop 有一定几率 会把 keyA 里面数据给拿出来,get 操作 有一定几率会拿到 keyB 的数据

    这是什么鬼。。。。。

    第 1 条附言  ·  2019-08-01 16:34:08 +08:00
    知道原因了,

    jedis 本身不是线程安全的,依靠 jedisPool 来为每一个 connection 创立连接保证线程安全

    可能是我的 jedisPool 的 maxTotal 设置的太小了,

    导致并发高的时候被复用了连接,然后就出现了上面的那个诡异的情况?但是为什么 key 也能被复用啊?


    https://blog.csdn.net/achenyuan/article/details/79312449
    这篇文章里面说 jedis 在出现异常的时候会缓存上一次的数据,我这边好像没有异常
    第 2 条附言  ·  2019-08-01 17:26:51 +08:00
    跟我的 getJedis() 函数 的 finally 那里错误的提早释放了实例有关系
    13L 大佬有指出来
    第 3 条附言  ·  2019-08-01 23:19:18 +08:00
    已经找到问题所在了

    从 jedisPool 里面获取 jedis 实例的时候,老代码迷之操作直接手动释放了实例到连接池中(此时可能还没使用或者正在使用)

    导致实例被其他连接重用(具体可以见 jedis 的 close 方法,里面可能对这种正在被使用的连接有特殊判断)


    正确做法应该是,

    获取连接,使用连接,使用完毕后释放(finally 中执行)
    31 条回复    2019-08-02 11:43:22 +08:00
    Buffer2Disk
        1
    Buffer2Disk  
    OP
       2019-08-01 15:49:57 +08:00
    使用的可以是这种格式的

    keyA queue:test

    keyB data:overload:user
    Buffer2Disk
        2
    Buffer2Disk  
    OP
       2019-08-01 15:50:32 +08:00
    抽风了, 使用的 key 是上面这种格式的
    ke1e
        3
    ke1e  
       2019-08-01 15:59:26 +08:00 via Android
    这是 jedis 的问题吧
    arrow8899
        4
    arrow8899  
       2019-08-01 16:04:53 +08:00
    发一下代码吧,估计是代码写的有问题。
    rrfeng
        5
    rrfeng  
       2019-08-01 16:05:51 +08:00 via Android
    用了同一个 connection 吧,估计是用错了。
    msg7086
        6
    msg7086  
       2019-08-01 16:11:32 +08:00
    看上去复用了一个网络连接,所以线程 1 发出请求的回答被线程 2 收走了。
    pelloz
        7
    pelloz  
       2019-08-01 16:12:53 +08:00
    请用 Lettuce
    Buffer2Disk
        8
    Buffer2Disk  
    OP
       2019-08-01 16:13:02 +08:00
    @rrfeng
    @arrow8899
    @ke1e

    private static void initialPool() {
    ApplicationContext applicationContext =
    new FileSystemXmlApplicationContext("classpath:config/spring-application-redis.xml");
    jedisPool = (JedisPool) applicationContext.getBean("jedisPool");
    logger.info("<---jedis pool init--->");
    }

    /**
    * 初始化,加锁防止被多次初始化 pool
    */
    private synchronized static void poolInit() {
    if (jedisPool == null) {
    initialPool();
    }
    }

    /**
    * 获取 Jedis 实例
    *
    * @return Jedis
    */
    public static Jedis getJedis() {
    poolInit();
    Jedis jedis = null;
    try {
    if (jedisPool != null) {
    jedis = jedisPool.getResource();
    }
    } catch (Exception e) {
    logger.error("get jedis error",e);
    } finally {
    returnResource(jedis);
    logger.debug("<---jedis return resource--->");
    }
    return jedis;
    }


    /**
    * 释放 jedis 资源
    *
    * @param jedis
    */
    public static void returnResource(final Jedis jedis) {
    if (jedis != null) {
    jedis.close();
    }
    }


    public static String get(String key) {
    try {
    return getJedis().get(key);
    } catch (Exception ex) {
    logger.error("redis get error",ex);
    }
    return null;
    }


    public static String lpop(String key) {
    try {
    return getJedis().lpop(key);
    } catch (Exception ex) {
    logger.error("lpop error , key = {}", key, ex);
    }
    return null;
    }
    cheng6563
        9
    cheng6563  
       2019-08-01 16:14:29 +08:00 via iPhone
    jedis 是线程不安全的
    请使用 redisson
    Buffer2Disk
        10
    Buffer2Disk  
    OP
       2019-08-01 16:14:51 +08:00
    @msg7086 还有复用同一个网络连接这么一说
    Buffer2Disk
        11
    Buffer2Disk  
    OP
       2019-08-01 16:19:09 +08:00
    @cheng6563
    我看网上说直接使用 jedis 是线程不安全的,但是使用 JedisPool 是线程安全的,
    我使用的就是 jedisPool 来获取 jedis 实例



    Jedis 不是线程安全的,故不应该在多线程环境中共用一个 Jedis 实例。但是,也应该避免直接创建多个 Jedis 实例,因为这种做法会导致创建过多的 socket 连接,性能不高。
    要保证线程安全且获得较好的性能,可以使用 JedisPool。JedisPool 是一个连接池,既可以保证线程安全,又可以保证了较高的效率。
    可以声明一个全局的 JedisPool 变量来保存 JedisPool 对象的引用,然后在其他地方使用。要知道,JedisPool 是一个线程安全的连接池。
    jorneyr
        12
    jorneyr  
       2019-08-01 16:31:51 +08:00
    } finally {
    returnResource(jedis);
    logger.debug("<---jedis return resource--->");
    }
    return jedis;

    这是什么操作,返回后马上把 redis 关闭
    Jrue0011
        13
    Jrue0011  
       2019-08-01 16:42:14 +08:00
    看问题是拿到了同一个 jedis
    原因应该是出在你获取 jedis 的代码,获取了 jedis 你还没用,你就在 finally 处调用了 jedis.close 将连接返回了连接池,导致连接池存在可用的 jedis,不会创建新对象,下一次拿到的还是相同的 jedis
    LeeSeoung
        14
    LeeSeoung  
       2019-08-01 16:42:29 +08:00
    关注,有大佬找到原因的话告知下。。
    glues
        15
    glues  
       2019-08-01 16:51:12 +08:00
    这一看就是 jedis 的问题,或者是你用的不对
    Buffer2Disk
        16
    Buffer2Disk  
    OP
       2019-08-01 16:54:35 +08:00
    知道
    @jorneyr 尴尬,应该是
    public static Jedis getJedis() {
    poolInit();
    Jedis jedis = null;
    try {
    if (jedisPool != null) {
    jedis = jedisPool.getResource();
    }
    } catch (Exception e) {
    logger.error("get jedis error", e);
    returnResource(jedis);
    logger.debug("<---jedis return resource--->");
    }
    return jedis;
    }
    Aresxue
        17
    Aresxue  
       2019-08-01 16:55:53 +08:00
    歪个楼,初始化 JedisPool 用单例的安全检查或者静态内部类,别加锁。。。Jedis 不是熟,但是 Lettuce 现在 shi 更流行也更好用,所以换 Lettuce 是最好的方式。
    Buffer2Disk
        18
    Buffer2Disk  
    OP
       2019-08-01 16:55:54 +08:00
    @Jrue0011 大佬,为什么拿到相同的 jedis,使用的 key 是不同的,为什么会拿到别的 key 的结果呢?
    Jrue0011
        19
    Jrue0011  
       2019-08-01 17:07:29 +08:00
    @Buffer2Disk 额。。你追踪下 Jedis 源码就知道了。一个 jedis -> client -> connection 维护一个 outputStream 和一个 inputStream,命令用 outputStream 发送,响应从 inputStream 读取,发送命令和读取响应的方法没加锁,一个线程读到另一个线程本该接收的响应很正常。。
    Buffer2Disk
        20
    Buffer2Disk  
    OP
       2019-08-01 17:56:16 +08:00
    @Jrue0011 改了以后确实可以了,有一个问题就是,jedis close() 后难道不是立马就释放回连接池的嘛?

    我试了下循环一百次 get 操作,很容易就触发到 maxTotal 的阈值了,然后程序就一直 hang 在那里,像是在等待连接池释放新的连接出来一样,但是等了很久也没有出来,这个是有什么参数可以控制的么
    Buffer2Disk
        21
    Buffer2Disk  
    OP
       2019-08-01 18:01:41 +08:00
    @Jrue0011 我看到有个 maxWaitMillis
    当资源池连接用尽后,调用者的最大等待时间(单位为毫秒),默认值 -1:表示永不超时

    有没有什么参数可以控制 close 后,多久返回连接池的。,。。。

    /**
    * 获取 Jedis 实例
    *
    * @return Jedis
    */
    public static Jedis getJedis() {
    poolInit();
    Jedis jedis = null;
    try {
    if (jedisPool != null) {
    jedis = jedisPool.getResource();
    }
    } catch (Exception e) {
    logger.error("get jedis error", e);
    returnResource(jedis);
    logger.debug("<---jedis return resource--->");
    }
    return jedis;
    }


    public static String get(String key) {
    Jedis jedis = null;
    try {
    jedis = getJedis();
    return jedis.get(key);
    } catch (Exception ex) {
    logger.error("redis get error", ex);
    } finally {
    returnResource(jedis);
    }
    return null;
    }

    /**
    * 释放 jedis 资源
    *
    * @param jedis
    */
    public static void returnResource(final Jedis jedis) {
    if (jedis != null) {
    jedis.close();
    }
    }
    leafre
        22
    leafre  
       2019-08-01 18:06:12 +08:00
    有安全问题,用 jedispool
    Buffer2Disk
        23
    Buffer2Disk  
    OP
       2019-08-01 18:13:50 +08:00
    @leafre 注意审题啊,我用的就是 jedisPool,只不过现在查出来是我的用法不太对
    Jrue0011
        24
    Jrue0011  
       2019-08-01 20:12:29 +08:00
    @Buffer2Disk 没有设置调用 close()之后多久返回连接池的这种设定。。。只要调用 jedis.close(),内部做一些处理就会返回对象池了,你调用 100 次之后的阻塞应该是别的问题。
    Buffer2Disk
        25
    Buffer2Disk  
    OP
       2019-08-01 20:51:55 +08:00
    @Jrue0011 但是我测试以后,这个阻塞的上限确实是一直跟着 maxTotal 变化的,给人的感觉就像是用完以后没有释放回连接池里面一样。。。。但是你看我上面的代码,调用 get(key) 后确实 finally 里面 close()了
    Buffer2Disk
        26
    Buffer2Disk  
    OP
       2019-08-01 21:08:03 +08:00
    @Jrue0011 我设置了 maxWaitMillis 为 2 秒,当到达 maxTotal 2 秒后,抛出了这个错误

    edis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool

    说明确实拿不到新的实例了,那么为啥没释放回去呢。。。。
    Buffer2Disk
        27
    Buffer2Disk  
    OP
       2019-08-01 21:13:12 +08:00
    @Jrue0011

    我明白了,是我自己傻逼了,测试的这个新方法没去释放连接,应该没问题了

    我看楼上他们说用 lettuce 这个玩意,内部用了 netty,可以复用连接

    不知道大佬你用过没有?有没有坑啊
    kppwp
        28
    kppwp  
       2019-08-02 08:28:26 +08:00 via iPhone
    redis 我记得都是原子操作
    Buffer2Disk
        29
    Buffer2Disk  
    OP
       2019-08-02 10:15:33 +08:00
    @kppwp 注意审题哈,这里其实跟 redis 其实没啥关系, 是我操作 jedis 的去连接 redis 的方式不对,

    导致并发场景下,向 redis 发送请求的数据被污染了
    razertory
        30
    razertory  
       2019-08-02 10:39:48 +08:00
    你可以认为 redis 中的指令没有并发安全问题。但是外部并发地去调用 redis 服务就需要自己去控制了
    Joyboo
        31
    Joyboo  
       2019-08-02 11:43:22 +08:00
    每个线程使用不同的 redis 连接
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2678 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 03:33 · PVG 11:33 · LAX 19:33 · JFK 22:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.