动态 Druid 连接池

冯旭光 Lv4

动态 Druid 连接池

背景

需求:因项目业务需求,需要在系统上配置不同数据库,然后进行连接获取数据库相关数据; 原生 JDBC 通过 DriverManager 进行连接每次都要重新进行连接,导致整体查询时间效率变低,故引入 Druid 连接池;
  但是不同类型数据库生成DruidDataSource后没有将DS(DruidDataSource)进行缓存起来, 本质还是每次连接数据库时还是重新进行数据库连接,问题依然存在。
  所以设计了一个容器DS缓存起来,考虑到容器里数量限制,使用LinkedHashMap进行存储数据,控制存储DS的数量。 另考虑缓存起来的DS存在数据库密码修改的情况,支持更新容器DS密码,保证 DS 可用。
具体代码如下:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

/**
* 连接池工具类
* Created by fengxuguang on 2023/12/28 11:54
*/
@Slf4j
public class ConnectionPoolUtil {
/**
* 初始化大小, 设置为 10
*/
private static final int INITIAL_SIZE = 10;
/**
* 最小线程数
*/
private static final int MIN_IDLE = 10;
/**
* 最大线程数
*/
private static final int MAX_ACTIVE = 200;
/**
* 连接等待超时时间, 单位: 毫秒
*/
private static final int MAX_WAIT = 60_000;
/**
* 间隔多久检测一次需要关闭的空闲连接, 单位: 毫秒
*/
private static final int TIME_BETWEEN_EVICTION_RUNS_MILLIS = 200_000;
/**
* 重连次数
*/
private static final int CONNECTION_ERRORS_RETRY_COUNT = 3;

/**
* 配置一个空闲连接在池中最小生存的时间, 单位是毫秒, 设置 5 分钟
*/
private static final int MIN_EVICT_TABLE_IDLE_TIME_MILLIS = 5 * 60 * 1000;

/**
* 连接池容器最大数量
*/
private static final int CONTAINER_MAX_SIZE = 5;

/**
* 连接池容器
*/
private static final Map<DbSourceKey, DruidDataSource> containers = new ContainerLinkedHashMap(CONTAINER_MAX_SIZE);

/**
* 获取 DataSource
* @param driverName 连接 driver-class
* @param connStr 连接 url
* @param username 账号
* @param password 密码
* @param validateQuery 校验查询语句
* @return DruidDataSource
*/
public synchronized static DruidDataSource getDataSource(String driverName, String connStr, String username, String password,
String validateQuery) {
try {
DbSourceKey dbSourceKey = new DbSourceKey(connStr, username, password);
if (containers.containsKey(dbSourceKey)) {
return getDruidDataSource(dbSourceKey);
}

containers.put(dbSourceKey, createDruidDataSource(driverName, connStr, username, password, validateQuery));

return containers.get(dbSourceKey);
} catch (Exception e) {
log.error("创建数据源连接池失败:{}", e.getMessage(), e);
}
}

/**
* 创建 DruidDataSource 对象
* @param driverName driver-class
* @param connStr 连接 url
* @param username 账号
* @param password 密码
* @param validateQuery 保持连接SQL
* @return DruidDataSource
*/
private static DruidDataSource createDruidDataSource(String driverName, String connStr, String username, String password,
String validateQuery) {
DruidDataSource ds = new DruidDataSource();

// 基础连接信息
ds.setDriverClassName(driverName);
ds.setUrl(connStr);
ds.setUsername(username);
ds.setPassword(password);

// 初始化大小
ds.setInitialSize(INITIAL_SIZE);
ds.setMinIdle(MIN_IDLE);
ds.setMaxActive(MAX_ACTIVE);

// 连接泄露监测
ds.setRemoveAbandoned(true);
ds.setRemoveAbandonedTimeout(30);

// 配置时间
// 配置获取连接等待超时的时间
ds.setMaxWait(MAX_WAIT);
// 配置间隔多久才进行一次检测, 检测需要关闭的空闲连接, 单位: 毫秒
ds.setTimeBetweenEvictionRunsMillis(TIME_BETWEEN_EVICTION_RUNS_MILLIS);
// 设置重连次数
ds.setBreakAfterAcquireFailure(true);
ds.setConnectionErrorRetryAttempts(CONNECTION_ERRORS_RETRY_COUNT);

// 防止过期
if (StringUtils.isBlank(validateQuery)) {
validateQuery = "SELECT 'x'";
} else if ("-1".equals(validateQuery)) {
validateQuery = null;
}
ds.setValidationQuery(validateQuery);
ds.setTestWhileIdle(true);
ds.setTestOnBorrow(true);
ds.setMinEvictableIdleTimeMillis(MIN_EVICT_TABLE_IDLE_TIME_MILLIS);

return ds;
}

/**
* 根据 dbSourceKey 获取 DruidDataSource
* @param dbSourceKey dbSourceKey
* @return DruidDataSource
*/
private static DruidDataSource getDruidDataSource(DbSourceKey dbSourceKey) {
if (!containers.containsKey(dbSourceKey)) {
return null;
}
// verify whether the password is changed. if the password is changed, update the password in the DruidDataSource
if (!dbSourceKey.getPassword().equals(containers.get(dbSourceKey).getPassword())) {
containers.get(dbSourceKey).setPassword(dbSourceKey.getPassword());
}
return containers.get(dbSourceKey);
}

/**
* 容器链表, 用于存储 DruidDataSource, key: DbSourceKey, value: DruidDataSource
*/
private static class ContainerLinkedHashMap extends LinkedHashMap<DbSourceKey, DruidDataSource> {

/**
* 链接最大容量
*/
private final int maxSize;

ContainerLinkedHashMap(int maxSize) {
if (maxSize <= 0) {
this.maxSize = 1;
} else {
this.maxSize = maxSize;
}
}

@Override
public DruidDataSource get(Object key) {
return super.get(key);
}

@Override
protected boolean removeEldestEntry(Map.Entry<DbSourceKey, DruidDataSource> eldest) {
// 超过最大容量, 需要移除旧的数据
if (containers.size() > maxSize) {
DruidDataSource druidDataSource = eldest.getValue();
druidDataSource.close();
}

return containers.size() > maxSize;
}
}

/**
* 存储数据源信息
*/
private static class DbSourceKey {

/**
* 连接 url, 设置后不可修改
*/
private final String connStr;

/**
* 账号, 设置后不可修改
*/
private final String account;

/**
* 密码, 设置后密码可修改
*/
@Setter
@Getter
private String password;

DbSourceKey(String connStr, String account, String password) {
this.connStr = connStr;
this.account = account;
this.password = password;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DbSourceKey that = (DbSourceKey) o;
return Objects.equals(connStr, that.connStr) && Objects.equals(account, that.account);
}

@Override
public int hashCode() {
return Objects.hash(connStr, account);
}
}

}

扩展知识

  1. ContainerLinkedHashMap继承LinkedHashMap后重写removeEldestEntry方法的原因。
1
2
3
4
5
6
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
// 省略...
}

解释:由于LinkedHashMap继承HashMapLinkedHashMap在调用put方法时,实际是调用HashMapput方法,HashMapput方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 省略...
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

void afterNodeInsertion(boolean evict) { }
}

看源码可知,调用put方法后接着去调用putVal方法,在将数据插入到HashMap后,会调用afterNodeInsertion方法,默认HashMap对此方法不做处理。但是LinkedHashMapafterNodeInsertion此方法进行了处理,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{

void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
}

afterNodeInsertion方法内会调用removeEldestEntry方法来判断是否删除链表头数据,默认LinkedHashMapremoveEldestEntry返回false,表示不移除链表头元素。

ContainerLinkedHashMap继承LinkedHashMap后,可以对removeEldestEntry方法进行重新,根据链表内的容量来判断是否需要删除链表头元素,以达到控制链表容量的目的。

ps:removeEldestEntry方法在LinkedHashMap调用时传入的参数为链表的头元素。

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static class ContainerLinkedHashMap extends LinkedHashMap<DbSourceKey, DruidDataSource> {

/**
* 链接最大容量
*/
private final int maxSize;

@Override
protected boolean removeEldestEntry(Map.Entry<DbSourceKey, DruidDataSource> eldest) {
// 超过最大容量, 需要移除旧的数据
if (containers.size() > maxSize) {
DruidDataSource druidDataSource = eldest.getValue();
druidDataSource.close();
}

return containers.size() > maxSize;
}
}
  • 标题: 动态 Druid 连接池
  • 作者: 冯旭光
  • 创建于 : 2024-02-07 14:56:47
  • 更新于 : 2025-03-11 14:43:46
  • 链接: https://blog.fengxuguang.top/posts/ecd042ad/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
动态 Druid 连接池