连接池:别让连接池帮了倒忙

本文深入探讨了连接池的结构和使用,强调了鉴别客户端SDK是否基于连接池的重要性。以数据库连接池、Redis连接池和HTTP连接池为例,阐述了连接池的配置和使用误区,特别是强调了连接池的复用和配置参数的动态调整。文章通过实例分析了Jedis的连接池实现,揭示了多线程环境下不正确使用连接池可能导致的问题,并提供了最佳实践。同时,文章提醒开发者在使用连接池时要注意监控和适时调整参数,以应对不同压力场景。
摘要由CSDN通过智能技术生成

今天,我再与你说说另一种很重要的池化技术,即连接池。

我先和你说说连接池的结构。连接池一般对外提供获得连接、归还连接的接口给客户端使用,并暴露最小空闲连接数、最大连接数等可配置参数,在内部则实现连接建立、连接心跳保持、连接管理、空闲连接回收、连接可用性检测等功能。连接池的结构示意图,如下所示:

38bec6e7ef3bf97b29b2c34ee29323b6.png

业务项目中经常会用到的连接池,主要是数据库连接池、Redis连接池和HTTP连接池。所以,今天我就以这三种连接池为例,和你聊聊使用和配置连接池容易出错的地方。

注意鉴别客户端SDK是否基于连接池

在使用三方客户端进行网络通信时,我们首先要确定客户端SDK是否是基于连接池技术实现的。我们知道,TCP是面向连接的基于字节流的协议:

  • 面向连接,意味着连接需要先创建再使用,创建连接的三次握手有一定开销;
  • 基于字节流,意味着字节是发送数据的最小单元,TCP协议本身无法区分哪几个字节是完整的消息体,也无法感知是否有多个客户端在使用同一个TCP连接,TCP只是一个读写数据的管道。

如果客户端SDK没有使用连接池,而直接是TCP连接,那么就需要考虑每次建立TCP连接的开销,并且因为TCP基于字节流,在多线程的情况下对同一连接进行复用,可能会产生线程安全问题

我们先看一下涉及TCP连接的客户端SDK,对外提供API的三种方式。在面对各种三方客户端的时候,只有先识别出其属于哪一种,才能理清楚使用方式。

  • 连接池和连接分离的API:有一个XXXPool类负责连接池实现,先从其获得连接XXXConnection,然后用获得的连接进行服务端请求,完成后使用者需要归还连接。通常,XXXPool是线程安全的,可以并发获取和归还连接,而XXXConnection是非线程安全的。对应到连接池的结构示意图中,XXXPool就是右边连接池那个框,左边的客户端是我们自己的代码。
  • 内部带有连接池的API:对外提供一个XXXClient类,通过这个类可以直接进行服务端请求;这个类内部维护了连接池,SDK使用者无需考虑连接的获取和归还问题。一般而言,XXXClient是线程安全的。对应到连接池的结构示意图中,整个API就是蓝色框包裹的部分。
  • 非连接池的API:一般命名为XXXConnection,以区分其是基于连接池还是单连接的,而不建议命名为XXXClient或直接是XXX。直接连接方式的API基于单一连接,每次使用都需要创建和断开连接,性能一般,且通常不是线程安全的。对应到连接池的结构示意图中,这种形式相当于没有右边连接池那个框,客户端直接连接服务端创建连接。

虽然上面提到了SDK一般的命名习惯,但不排除有一些客户端特立独行,因此在使用三方SDK时,一定要先查看官方文档了解其最佳实践,或是在类似Stackoverflow的网站搜索XXX threadsafe/singleton字样看看大家的回复,也可以一层一层往下看源码,直到定位到原始Socket来判断Socket和客户端API的对应关系。

明确了SDK连接池的实现方式后,我们就大概知道了使用SDK的最佳实践:

  • 如果是分离方式,那么连接池本身一般是线程安全的,可以复用。每次使用需要从连接池获取连接,使用后归还,归还的工作由使用者负责。
  • 如果是内置连接池,SDK会负责连接的获取和归还,使用的时候直接复用客户端。
  • 如果SDK没有实现连接池(大多数中间件、数据库的客户端SDK都会支持连接池),那通常不是线程安全的,而且短连接的方式性能不会很高,使用的时候需要考虑是否自己封装一个连接池。

接下来,我就以Java中用于操作Redis最常见的库Jedis为例,从源码角度分析下Jedis类到底属于哪种类型的API,直接在多线程环境下复用一个连接会产生什么问题,以及如何用最佳实践来修复这个问题。

首先,向Redis初始化2组数据,Key=a、Value=1,Key=b、Value=2:

@PostConstructpublic void init() { try (Jedis jedis = new Jedis("127.0.0.1", 6379)) { Assert.isTrue("OK".equals(jedis.set("a", "1")), "set a = 1 return OK"); Assert.isTrue("OK".equals(jedis.set("b", "2")), "set b = 2 return OK"); }}

然后,启动两个线程,共享操作同一个Jedis实例,每一个线程循环1000次,分别读取Key为a和b的Value,判断是否分别为1和2:

Jedis jedis = new Jedis("127.0.0.1", 6379);new Thread(() -> { for (int i = 0; i < 1000; i++) { String result = jedis.get("a"); if (!result.equals("1")) { log.warn("Expect a to be 1 but found {}", result); return; } }}).start();new Thread(() -> { for (int i = 0; i < 1000; i++) { String result = jedis.get("b"); if (!result.equals("2")) { log.warn("Expect b to be 2 but found {}", result); return; } }}).start();TimeUnit.SECONDS.sleep(5);

执行程序多次,可以看到日志中出现了各种奇怪的异常信息,有的是读取Key为b的Value读取到了1,有的是流非正常结束,还有的是连接关闭异常:

//错误1[14:56:19.069] [Thread-28] [WARN ] [.t.c.c.redis.JedisMisreuseController:45 ] - Expect b to be 2 but found 1//错误2redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream. at redis.clients.jedis.util.RedisInputStream.en
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值