[TOC]
# db-spring-boot-starter
我们将采用springboot 标准starter的做法开发项目基础组件,利用org.springframework.boot.autoconfigure,完成对象的基本装配。同时他具有以下功能:
* druid数据源
* mybatis-plus
* sharding-jdbc
* 字段填充插件
* 多租户插件
* sql执行时间监控插件
* 查询大结果集监控插件
* 敏感数据脱敏插件
* pagehelper分页处理
## maven 相关依赖引入
![](https://img.kancloud.cn/2e/a8/2ea857c2e6d8a6c0be25348974174289_1610x1141.png)
## druid数据源
* druid 连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。
### 数据源竞品分析
![](https://img.kancloud.cn/b5/28/b52816bc693b0e7ecbb2365ac4f02770_924x581.png)
### druid 使用方式
平台本身没有重复造轮子,而是做整合,
* druid-spring-boot-starter
* dynamic-datasource-spring-boot-starter
* sharding-jdbc-spring-boot-starter
![](https://img.kancloud.cn/ce/09/ce098317b19194716f4a0189bee97775_2178x739.png)
*单数据源配置
```
spring:
session:
store-type: none
datasource:
druid:
url: jdbc:mysql://${ocp.datasource.ip:192.168.92.216}:3306/oauth-center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: ${ocp.datasource.username}
password: ${ocp.datasource.password}
driver-class-name: com.mysql.cj.jdbc.Driver
#连接池配置(通常来说,只需要修改initialSize、minIdle、maxActive
initial-size: 5
max-active: 50
min-idle: 5
# 配置获取连接等待超时的时间
max-wait: 60000
#打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
validation-query: SELECT 'x'
test-on-borrow: false
test-on-return: false
test-while-idle: true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
#配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
filter:
stat:
enabled: true
wall:
config:
multi-statement-allow: true
# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
#是否启用StatFilter默认值true
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
# StatViewServlet配置
#展示Druid的统计信息,StatViewServlet的用途包括:1.提供监控信息展示的html页面2.提供监控信息的JSON API
#是否启用StatViewServlet默认值true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: true
login-username: admin
login-password: admin
#根据配置中的url-pattern来访问内置监控页面,如果是上面的配置,内置监控页面的首页是/druid/index.html例如:
#http://110.76.43.235:9000/druid/index.html
#http://110.76.43.235:8080/mini-web/druid/index.html
#允许清空统计数据
#StatViewSerlvet展示出来的监控信息比较敏感,是系统运行的内部情况,如果你需要做访问控制,可以配置allow和deny这两个参数
#deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝。如果allow没有配置或者为空,则允许所有访问
#配置的格式
#<IP>
#或者<IP>/<SUB_NET_MASK_size>其中128.242.127.1/24
#24表示,前面24位是子网掩码,比对的时候,前面24位相同就匹配,不支持IPV6。
#stat-view-servlet.allow=
#stat-view-servlet.deny=128.242.127.1/24,128.242.128.1
# Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置
#aop-patterns= # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
mybatis-plus:
mapper-locations: com/open/**/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.open.capacity.oauth.model
global-config:
banner: false
db-config:
id-type: auto
```
* 多数据源配置
```
spring:
datasource:
dynamic:
enabled: true
datasource:
master:
url: jdbc:mysql://${ocp.datasource.ip:192.168.92.216}:3306/oauth-center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: ${ocp.datasource.username}
password: ${ocp.datasource.password}
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://${ocp.datasource.ip:192.168.92.216}:3306/user-center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: ${ocp.datasource.username}
password: ${ocp.datasource.password}
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
filter:
stat:
enabled: true
wall:
config:
multi-statement-allow: true
# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
#是否启用StatFilter默认值true
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
# StatViewServlet配置
#展示Druid的统计信息,StatViewServlet的用途包括:1.提供监控信息展示的html页面2.提供监控信息的JSON API
#是否启用StatViewServlet默认值true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: true
login-username: admin
login-password: admin
mybatis-plus:
mapper-locations: com/open/**/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.open.capacity.oauth.model
global-config:
banner: false
db-config:
id-type: auto
```
* sharding配置如下
```
spring:
shardingsphere:
enabled: false
datasource:
druid:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${ocp.datasource.ip:192.168.92.216}:3306/user-center?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: ${ocp.datasource.username}
password: ${ocp.datasource.password}
#初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initial-size: 5
#最大连接数
max-active: 50
#最小连接数
min-idle: 5
#获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
max-wait: 60000
#用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validation-query: SELECT 1 FROM DUAL
#单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
validation-query-timeout: 5
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-borrow: false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false
#有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
time-between-eviction-runs-millis: 60000
# 连接保持空闲而不被驱逐的最小时间
min-evictable-idle-time-millis: 300000
#连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
keep-alive: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
#是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
pool-prepared-statements: false
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: 100
#是否到期强制删除,避免某个连接长时间阻塞无法回收
remove-abandoned: true
#租用时长,Druid避免连泄露 s
remove-abandoned-timeout: 120
```
## druid原理解析
### 1.主流程:获取连接流程
首先从入口来看看它在获取连接时做了哪些操作:
![](https://img.kancloud.cn/18/91/18917e5a10f78b32c2192d15ac376d5a_1155x1043.png)
* 上述为获取连接时的流程图,首先会调用init进行连接池的初始化,然后运行责任链上的每一个filter,最终执行getConnectionDirect获取真正的连接对象,如果开启了testOnBorrow,则每次都会去测试连接是否可用(这也是官方不建议设置testOnBorrow为true的原因,影响性能,这里的测试是指测试mysql服务端的长连接是否断开,一般mysql服务端长连保活时间是8h,被使用一次则刷新一次使用时间,若一个连接距离上次被使用超过了保活时间,那么再次使用时将无法与mysql服务端通信)。
* 如果testOnBorrow没有被置为true,则会进行testWhileIdle的检查(这一项官方建议设置为true,缺省值也是true),检查时会判断当前连接对象距离上次被使用的时间是否超过规定检查的时间,若超过,则进行检查一次,这个检查时间通过timeBetweenEvictionRunsMillis来控制,默认60s。
* 每个连接对象会记录下上次被使用的时间,用当前时间减去上一次的使用时间得出闲置时间,闲置时间再跟timeBetweenEvictionRunsMillis比较,超过这个时间就做一次连接可用性检查,这个相比testOnBorrow每次都检查来说,性能会提升很多,用的时候无需关注该值,因为缺省值是true,经测试如果将该值设置为false,testOnBorrow也设置为false,数据库服务端长连保活时间改为60s,60s内不使用连接,超过60s后使用将会报连接错误。
* 若使用testConnectionInternal方法测试长连接结果为false,则证明该连接已被服务端断开或者有其他的网络原因导致该连接不可用,则会触发discardConnection进行连接回收(对应流程1.4,因为丢弃了一个连接,因此该方法会唤醒主流程3进行检查是否需要新建连接)。整个流程运行在一个死循环内,直到取到可用连接或者超过重试上限报错退出(在连接没有超过连接池上限的话,最多重试一次(重试次数默认重试1次,可以通过notFullTimeoutRetryCount属性来控制),所以取连接这里一旦发生等待,在连接池没有满的情况下,最大等待 2 × maxWait 的时间 ←这个有待验证)。
#### 特别说明①
为了保证性能,不建议将testOnBorrow设置为true,或者说牵扯到长连接可用检测的那几项配置使用druid默认的配置就可以保证性能是最好的,如上所说,默认长连接检查是60s一次,所以不启用testOnBorrow的情况下要想保证万无一失,自己要确认下所连的那个mysql服务端的长连接保活时间(虽然默认是8h,但是dba可能给测试环境设置的时间远小于这个时间,所以如果这个时间小于60s,就需要手动设置timeBetweenEvictionRunsMillis了,如果mysql服务端长连接时间是8h或者更长,则用默认值即可。
#### 特别说明②
为了防止不必要的扩容,在mysql服务端长连接够用的情况下,对于一些qps较高的服务、网关业务,建议把池子的最小闲置连接数minIdle和最大连接数maxActive设置成一样的,且按照需要调大,且开启keepAlive进行连接活性检查(参考流程4.1),这样就不会后期发生动态新建连接的情况(建连还是个比较重的操作,所以不如一开始就申请好所有需要的连接,个人意见,仅供参考),但是像管理后台这种,长期qps非常低,但是有的时候需要用管理后台做一些巨大的操作(比如导数据什么的)导致需要的连接暴增,且管理后台不会特别要求性能,就适合将minIdle的值设置的比maxActive小,这样不会造成不必要的连接浪费,也不会在需要暴增连接的时候无法动态扩增连接。
### 2.主流程:初始化连接池
通过上面的流程图可以看到,在获取一个连接的时候首先会检查连接池是否已经初始化完毕(通过inited来控制,bool类型,未初始化为flase,初始化完毕为true,这个判断过程在init方法内完成),若没有初始化,则调用init进行初始化(图主流程1中的紫色部分),下面来看看init方法里又做了哪些操作:
![](https://img.kancloud.cn/61/f6/61f606bfa1a07aa01ded21744d0edaee_1005x1054.png)
可以看到,实例化的时候会初始化全局的重入锁lock,在初始化过程中包括后续的连接池操作都会利用该锁保证线程安全,初始化连接池的时候首先会进行双重检查是否已经初始化过,若没有,则进行连接池的初始化,这时候还会通过SPI机制额外加载责任链上的filter。
但是这类filter需要在类上加上@AutoLoad注解。然后初始化了三个数组,容积都为maxActive,首先connections就是用来存放池子里连接对象的,evictConnections用来存放每次检查需要抛弃的连接(结合流程4.1理解),keepAliveConnections用于存放需要连接检查的存活连接(同样结合流程4.1理解),然后生成初始化数(initialSize)个连接,放进connections,然后生成两个必须的守护线程,用来添加连接进池以及从池子里摘除不需要的连接,这俩过程较复杂,因此拆出来单说(主流程3和主流程4)。
特别说明①
* 从流程上看如果一开始实例化的时候不对连接池进行初始化(这个初始化是指对池子本身的初始化,并非单纯的指druid对象属性的初始化),那么在第一次调用getConnection时就会走上图那么多逻辑,尤其是耗时较久的建立连接操作,被重复执行了很多次,导致第一次getConnection时耗时过久,如果你的程序并发量很大,那么第一次获取连接时就会因为初始化流程而发生排队,所以建议在实例化连接池后对其进行预热,通过调用init方法或者getConnection方法都可以。
特别说明②
在构建全局重入锁的时候,利用lock对象生成了俩Condition,对这俩Condition解释如下:
当连接池连接够用时,利用empty阻塞添加连接的守护线程(主流程3),当连接池连接不够用时,获取连接的那个线程(这里记为业务线程A)就会阻塞在notEmpty上,且唤起阻塞在empty上的添加连接的守护线程,走完添加连接的流程,走完后会重新唤起阻塞在notEmpty上的业务线程A,业务线程A就会继续尝试获取连接。
#### 流程1.1:责任链
WARN:这块东西结合源码看更容易理解
![](https://img.kancloud.cn/5c/ad/5cad31ca5bb1baae7d2a95d71c2475f7_1315x1005.png)
这里对应流程1里获取连接时需要执行的责任链,每个DruidAbstractDataSource里都包含filters属性,filters是对Druid里Filters接口的实现,里面有很多对应着连接池里的映射方法,比如例子中dataSource的getConnection方法在触发的时候就会利用FilterChain把每个filter里的dataSource\_getConnection给执行一遍,这里也要说明下FilterChain,通过流程1.1可以看出来,datasource是利用FilterChain来触发各个filter的执行的,FilterChain里也有一堆datasource里的映射方法,比如上图里的dataSource\_connect,这个方法会把datasource里的filters全部执行一遍直到nextFilter取不到值,才会触发dataSource.getConnectionDirect,这个结合代码会比较容易理解。
#### 流程1.2:从池中获取连接的流程
![](https://img.kancloud.cn/44/1b/441b1b7644fe535eadeb658886a7f811_1155x646.png)
通过getConnectionInternal方法从池子里获取真正的连接对象,druid支持两种方式新增连接,一种是通过开启不同的守护线程通过await、signal通信实现(本文启用的方式,也是默认的方式),另一种是直接通过线程池异步新增,这个方式通过在初始化druid时传入asyncInit=true,再把一个线程池对象赋值给createScheduler,就成功启用了这种模式,没仔细研究这种方式,所以本文的流程图和代码块都会规避这个模式。
上面的流程很简单,连接足够时就直接poolingCount-1,数组取值,返回,activeCount+1,整体复杂度为O(1),关键还是看取不到连接时的做法,取不到连接时,druid会先唤起新增连接的守护线程新增连接,然后陷入等待状态,然后唤醒该等待的点有两处,一个是用完了连接recycle(主流程5)进池子后触发,另外一个就是新增连接的守护线程成功新增了一个连接后触发,await被唤起后继续加入锁竞争,然后往下走如果发现池子里的连接数仍然是0(说明在唤醒后参与锁竞争里刚被放进来的连接又被别的线程拿去了),则继续下一次的await,这里采用的是awaitNanos方法,初始值是maxWait,然后下次被刷新后就是maxWait减去上次阻塞花费的实际时间,每次await的时间会逐步减少,直到归零,整体时间是约等于maxWait的,但实际比maxActive要大,因为程序本身存在耗时以及被唤醒后又要参与锁竞争导致也存在一定的耗时。
如果最终都没办法拿到连接则返回null出去,紧接着触发主流程1中的重试逻辑。
druid如何防止在获取不到连接时阻塞过多的业务线程?
* 通过上面的流程图和流程描述,如果非常极端的情况,池子里的连接完全不够用时,会阻塞过多的业务线程,甚至会阻塞超过maxWait这么久,有没有一种措施是可以在连接不够用的时候控制阻塞线程的个数,超过这个限制后直接报错,而不是陷入等待呢?
* druid其实支持这种策略的,在maxWaitThreadCount属性为默认值(-1)的情况下不启用,如果maxWaitThreadCount配置大于0,表示启用,这是druid做的一种丢弃措施,如果你不希望在池子里的连接完全不够用导阻塞的业务线程过多,就可以考虑配置该项,这个属性的意思是说在连接不够用时最多让多少个业务线程发生阻塞,流程1.2的图里没有体现这个开关的用途,可以在代码里查看,每次在pollLast方法里陷入等待前会把属性notEmptyWaitThreadCount进行累加,阻塞结束后会递减,由此可见notEmptyWaitThreadCount就是表示当前等待可用连接时阻塞的业务线程的总个数,而getConnectionInternal在每次调用pollLast前都会判断这样一段代码:
```
if (maxWaitThreadCount \> 0 && notEmptyWaitThreadCount \>= maxWaitThreadCount) { connectErrorCountUpdater.incrementAndGet(this); throw new SQLException("maxWaitThreadCount " + maxWaitThreadCount + ", current wait Thread count " + lock.getQueueLength()); //直接抛异常,而不是陷入等待状态阻塞业务线程 }
```
可以看到,如果配置了maxWaitThreadCount所限制的等待线程个数,那么会直接判断当前陷入等待的业务线程是否超过了maxWaitThreadCount,一旦超过甚至不触发pollLast的调用(防止新增等待线程),直接抛错。
一般情况下不需要启用该项,一定要启用建议考虑好maxWaitThreadCount的取值,一般来说发生大量等待说明代码里存在不合理的地方:比如典型的连接池基本配置不合理,高qps的系统里maxActive配置过小;比如借出去的连接没有及时close归还;比如存在慢查询或者慢事务导致连接借出时间过久。这些要比配置maxWaitThreadCount更值得优先考虑,当然配置这个做一个极限保护也是没问题的,只是要结合实际情况考虑好取值。
#### 流程1.3:连接可用性测试
#### ①init-checker
讲这块的东西之前,先来了解下如何初始化检测连接用的checker,整个流程参考下图:
![](https://img.kancloud.cn/95/af/95af0f3614d9b9b9e3d8a15091b08956_1119x657.png)
初始化checker发生在init阶段(限于篇幅,没有在主流程2(init阶段)里体现出来,只需要记住初始化checker也是发生在init阶段就好),druid支持多种数据库的连接源,所以checker针对不同的驱动程序都做了适配,所以才看到图中checker有不同的实现,我们根据加载到的驱动类名匹配不同的数据库checker,上图匹配至mysql的checker,checker的初始化里做了一件事情,就是判断驱动内是否有ping方法(jdbc4开始支持,mysql-connector-java早在3.x的版本就有ping方法的实现了),如果有,则把usePingMethod置为true,用于后续启用checker时做判断用(下面会讲,这里置为true,则通过反射的方式调用驱动程序的ping方法,如果为false,则触发普通的SELECT 1查询检测,SELECT 1就是我们非常熟悉的那个东西啦,新建statement,然后执行SELECT 1,然后再判断连接是否可用)。
#### ②testConnectionInternal
然后回到本节探讨的方法:流程1.3对应的testConnectionInternal
![](https://img.kancloud.cn/d7/86/d7867505742b896d6644aea408faa507_1224x547.png)
这个方法会利用主流程2(init阶段)里初始化好的checker对象(流程参考init-checker)里的isValidConnection方法,如果启用ping,则该方法会利用invoke触发驱动程序里的ping方法,如果不启用ping,就采用SELECT 1方式(从init-checker里可以看出启不启用取决于加载到的驱动程序里是否存在相应的方法)。
#### 流程1.4:抛弃连接
![](https://img.kancloud.cn/b1/67/b16787008fe09ec5dc44fb2701c27390_1279x1011.png)
经过流程1.3返回的测试结果,如果发现连接不可用,则直接触发抛弃连接逻辑,这个过程非常简单,如上图所示,由流程1.2获取到该连接时累加上去的activeCount,在本流程里会再次减一,表示被取出来的连接不可用,并不能active状态。其次这里的close是拿着驱动那个连接对象进行close,正常情况下一个连接对象会被druid封装成DruidPooledConnection对象,内部持有的conn就是真正的驱动Connection对象,上图中的关闭连接就是获取的该对象进行close,如果使用包装类DruidPooledConnection进行close,则代表回收连接对象(recycle,参考主流程5)。
### 3.主流程:添加连接的守护线程
![](https://img.kancloud.cn/d6/1e/d61e867ee516702e8102597528e20c56_1234x876.png)
在主流程2(init初始化阶段)时就开启了该流程,该流程独立运行,大部分时间处于等待状态,不会抢占cpu,但是当连接不够用时,就会被唤起追加连接,成功创建连接后将会唤醒其他正在等待获取可用连接的线程,比如:
结合流程1.2来看,当连接不够用时,会通过empty.signal唤醒该线程进行补充连接(阻塞在empty上的线程只有主流程3的单线程),然后通过notEmpty阻塞自己,当该线程补充连接成功后,又会对阻塞在notEmpty上的线程进行唤醒,让其进入锁竞争状态,简单理解就是一个生产-消费模型。这里有一些细节,比如池子里的连接使用中(activeCount)加上池子里剩余连接数(poolingCount)就是指当前一共生成了多少个连接,这个数不能比maxActive还大,如果比maxActive还大,则再次陷入等待。而在往池子里put连接时,则判断poolingCount是否大于maxActive来决定最终是否入池。
### 4.主流程:抛弃连接的守护线程
![](https://img.kancloud.cn/ae/50/ae504423524fe22eb7ca914c01f93bdd_1285x961.png)
#### 流程4.1:连接池瘦身,检查连接是否可用以及丢弃多余连接
整个过程如下:
![](https://img.kancloud.cn/4c/30/4c30da270afb9c1e28a884dabc9639e8_1285x2046.png)
整个流程分成图中主要的几步,首先利用poolingCount减去minIdle计算出需要做丢弃检查的连接对象区间,意味着这个区间的对象有被丢弃的可能,具体要不要放进丢弃队列evictConnections,要判断两个属性:
minEvictableIdleTimeMillis:最小检查间隙,缺省值30min,官方解释:一个连接在池中最小生存的时间(结合检查区间来看,闲置时间超过这个时间,才会被丢弃)。
maxEvictableIdleTimeMillis:最大检查间隙,缺省值7h,官方解释:一个连接在池中最大生存的时间(无视检查区间,只要闲置时间超过这个时间,就一定会被丢弃)。
如果当前连接对象闲置时间超过minEvictableIdleTimeMillis且下标在evictCheck区间内,则加入丢弃队列evictConnections,如果闲置时间超过maxEvictableIdleTimeMillis,则直接放入evictConnections(一般情况下会命中第一个判断条件,除非一个连接不在检查区间,且闲置时间超过maxEvictableIdleTimeMillis)。
如果连接对象不在evictCheck区间内,且keepAlive属性为true,则判断该对象闲置时间是否超出keepAliveBetweenTimeMillis(缺省值60s),若超出,则意味着该连接需要进行连接可用性检查,则将该对象放入keepAliveConnections队列。
两个队列赋值完成后,则池子会进行一次压缩,没有涉及到的连接对象会被压缩到队首。
然后就是处理evictConnections和keepAliveConnections两个队列了,evictConnections里的对象会被close最后释放掉,keepAliveConnections里面的对象将会其进行检测(流程参考流程1.3的isValidConnection),碰到不可用的连接会调用discard(流程1.4)抛弃掉,可用的连接会再次被放进连接池。
整个流程可以看出,连接闲置后,也并非一下子就减少到minIdle的,如果之前产生一堆的连接(不超过maxActive),突然闲置了下来,则至少需要花minEvictableIdleTimeMillis的时间才可以被移出连接池,如果一个连接闲置时间超过maxEvictableIdleTimeMillis则必定被回收,所以极端情况下(比如一个连接池从初始化后就没有再被使用过),连接池里并不会一直保持minIdle个连接,而是一个都没有,生产环境下这是非常不常见的,默认的maxEvictableIdleTimeMillis都有7h,除非是极度冷门的系统才会出现这种情况,而开启keepAlive也不会推翻这个规则,keepAlive的优先级是低于maxEvictableIdleTimeMillis的,keepAlive只是保证了那些检查中不需要被移出连接池的连接在指定检测时间内去检测其连接活性,从而决定是否放入池子或者直接discard。
#### 流程4.2:主动回收连接,防止内存泄漏
过程如下:
![](https://img.kancloud.cn/42/69/42690d806854bf0b3eaaef4c6bd1341f_499x939.png)
这个流程在removeAbandoned设置为true的情况下才会触发,用于回收那些拿出去的使用长期未归还(归还:调用close方法触发主流程5)的连接。
先来看看activeConnections是什么,activeConnections用来保存当前从池子里被借出去的连接,这个可以通过主流程1看出来,每次调用getConnection时,如果开启removeAbandoned,则会把连接对象放到activeConnections,然后如果长期不调用close,那么这个被借出去的连接将永远无法被重新放回池子,这是一件很麻烦的事情,这将存在内存泄漏的风险,因为不close,意味着池子会不断产生新的连接放进connections,不符合连接池预期(连接池出发点是尽可能少的创建连接),然后之前被借出去的连接对象还有一直无法被回收的风险,存在内存泄漏的风险,因此为了解决这个问题,就有了这个流程,流程整体很简单,就是将现在借出去还没有归还的连接,做一次判断,符合条件的将会被放进abandonedList进行连接回收(这个list里的连接对象里的abandoned将会被置为true,标记已被该流程处理过,防止主流程5再次处理)。
这个如果在实践中能保证每次都可以正常close,完全不用设置removeAbandoned=true,目前如果使用了类似mybatis、spring等开源框架,框架内部是一定会close的,所以此项是不建议设置的,视情况而定。
### 5.主流程:回收连接
这个流程通常是靠连接包装类DruidPooledConnection的close方法触发的,目标方法为recycle,流程图如下:
![](https://img.kancloud.cn/69/06/6906a6307caf71a155948bf64ef93b57_1080x993.png)
这也是非常重要的一个流程,连接用完要归还,就是利用该流程完成归还的动作,利用druid对外包装的Connecion包装类DruidPooledConnection的close方法触发,该方法会通过自己内部的close或者syncClose方法来间接触发dataSource对象的recycle方法,从而达到回收的目的。
最终的recycle方法:
①如果removeAbandoned被设置为true,则通过traceEnable判断是否需要从activeConnections移除该连接对象,防止流程4.2再次检测到该连接对象,当然如果是流程4.2主动触发的该流程,那么意味着流程4.2里已经remove过该对象了,traceEnable会被置为false,本流程就不再触发remove了(这个流程都是在removeAbandoned=true的情况下进行的,在主流程1里连接被放进activeConnections时traceEnable被置为true,而在removeAbandoned=false的情况下traceEnable恒等于false)。
②如果回收过程中发现存在有未处理完的事务,则触发回滚(比较有可能触发这一条的是流程4.2里强制归还连接,也有可能是单纯使用连接,开启事务却没有提交事务就直接close的情况),然后利用holder.reset进行恢复连接对象里一些属性的默认值,除此之外,holder对象还会把由它产生的statement对象放到自己的一个arraylist里面,reset方法会循环着关闭内部未关闭的statement对象,最后清空list,当然,statement对象自己也会记录下其产生的所有的resultSet对象,然后关闭statement时同样也会循环关闭内部未关闭的resultSet对象,这是连接池做的一种保护措施,防止用户拿着连接对象做完一些操作没有对打开的资源关闭。
③判断是否开启testOnReturn,这个跟testOnBorrow一样,官方默认不开启,也不建议开启,影响性能,理由参考主流程1里针对testOnBorrow的解释。
④直接放回池子(当前connections的尾部),然后需要注意的是putLast方法和put方法的不同之处,putLast会把lastActiveTimeMillis置为当前时间,也就是说不管一个连接被借出去过久,只要归还了,最后活跃时间就是当前时间,这就会有造成某种特殊异常情况的发生(非常极端,几乎不会触发,可以选择不看):
如果不开启testOnBorrow和testOnReturn,并且keepAlive设置为false,那么长连接可用测试的间隔依据就是利用当前时间减去上次活跃时间(lastActiveTimeMillis)得出闲置时间,然后再利用闲置时间跟timeBetweenEvictionRunsMillis(默认60s)进行对比,超过才进行长连接可用测试。
那么如果一个mysql服务端的长连接保活时间被人为调整为60s,然后timeBetweenEvictionRunsMillis被设置为59s,这个设置是非常合理的,保证了测试间隔小于长连接实际保活时间,然后如果这时一个连接被拿出去后一直过了61s才被close回收,该连接对象的lastActiveTimeMillis被刷为当前时间,如果在59s内再次拿到该连接对象,就会绕过连接检查直接报连接不可用的错误。
### 10.结束
以上针对druid连接池的初始化以及其内部一个连接从生产到消亡的整个流程就已经整理完了,主要是列出其运行流程以及一些主要的监控数据都是如何产生的,没有涉及到的是一个sql的执行,因为这个基本上就跟使用原生驱动程序差不多,只是druid又包装了一层Statement等,用于完成一些自己的操作。
## mybatis-plus介绍
* MyBatis 是一款优秀的持久层框架,其目的是想当做互联网的篱笆墙,围绕着数据库提供持久化服务的一个框架,支持自定义 SQL、存储过程及高级映射。
* MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,还可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Ordinary Java Object,普通 Java 对象)为数据库中的记录。
* [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个[MyBatis](http://www.mybatis.org/mybatis-3/)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
### mybatis原理
![](https://img.kancloud.cn/86/4e/864ed15042ccc001b963f468471a0d77_1765x925.png)
### mybatis-plus配置方式
配置mybatis-plus全局配置,是否关闭启动mybatis-plus图标,id生成策略,扫描mapper.xml位置
```
mybatis-plus:
mapper-locations: com/open/**/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.open.capacity.common.model
global-config:
banner: false
db-config:
id-type: auto
```
### mybatis-plus简单使用
![](https://img.kancloud.cn/ad/60/ad60fa6e4632e8c19eee043f4a14d99b_2542x748.png)
## sharding-jdbc介绍
Sharding-JDBC是ShardingSphere的第一个产品,也是ShardingSphere的前身。 它定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
* 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
* 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
* 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
### sharding配置
shardingjdbc配置默认属于,采用雪花算法生成id方式
```
spring:
shardingsphere:
enabled: false
sharding:
default-data-source-name: ds0
default-key-generator:
column: id
props:
worker:
id: ${workerId}
type: SNOWFLAKE
datasource:
names: ds0
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${ocp.datasource.ip:192.168.92.216}:3306/user-center?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: ${ocp.datasource.username}
password: ${ocp.datasource.password}
#初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initial-size: 5
#最大连接数
max-active: 50
#最小连接数
min-idle: 5
#获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
max-wait: 60000
#用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validation-query: SELECT 1 FROM DUAL
#单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
validation-query-timeout: 5
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-borrow: false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false
#有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
time-between-eviction-runs-millis: 60000
# 连接保持空闲而不被驱逐的最小时间
min-evictable-idle-time-millis: 300000
#连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
keep-alive: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
#是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
pool-prepared-statements: false
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
max-pool-prepared-statement-per-connection-size: 100
#是否到期强制删除,避免某个连接长时间阻塞无法回收
remove-abandoned: true
#租用时长,Druid避免连泄露 s
remove-abandoned-timeout: 120
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
#合并多个DruidDataSource的监控数据
use-global-data-source-stat: false
#配置stat-view-servlet
stat-view-servlet:
#允许开启监控
enabled: true
#监控面板路径
url-pattern: /druid/*
```
### shardingjdbc雪花id生成配置
![](https://img.kancloud.cn/7b/23/7b23149db13f7660c7807b423b81a16f_1951x1036.png)
```
spring:
shardingsphere:
enabled: false
sharding:
default-data-source-name: ds0
default-key-generator:
column: id
props:
worker:
id: ${workerId}
type: SNOWFLAKE
```
## 字段填充
配置自动填充创建时间修改时间
![](https://img.kancloud.cn/31/a9/31a9079718b1fda3bc821afcd6e8d28e_1841x1215.png)
## 多租户应用隔离
当不同的租户使用同一套程序,这里就需要考虑一个数据隔离的情况。
数据隔离有三种方案:
独立数据库:简单来说就是一个租户使用一个数据库,这种数据隔离级别最高,安全性最好,但是提高成本。
共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个Schema(数据库user)。
共享数据库、共享数据架构:使用同一个数据库,同一个Schema,但是在表中增加了租户ID的字段,这种共享数据程度最高,隔离级别最低。平台采用mybatis-plus多租户插件,进行oauth体系的应用隔离方式。
* 不同应用系统之间是完全隔离的!!!
* 启用多租户后所有执行的method的sql都会进行处理.
* 自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
### 多租插件介绍
![](https://img.kancloud.cn/3f/20/3f20a22fa401c3f67533b8abff5c9e8c_998x132.png)
### 租户自动装配
![](https://img.kancloud.cn/6f/e2/6fe22507dbc0307a7982c6133918000a_1940x1210.png)
### 多租户拦截器
![](https://img.kancloud.cn/05/ef/05efec809402344d9c087dd9e4084229_2224x1068.png)
### 租户配置
```
ocp:
#多租户配置
tenant:
enable: true
ignoreTables:
- sys_user
- sys_role_user
- sys_role_menu
ignoreSqls:
# 用户关联角色时,显示所有角色
- SysRoleMapper.findAll
# 用户列表显示用户所关联的所有角色
- SysUserRoleMapper.findRolesByUserIds
```
## sql执行时间监控插件
系统运行期间,可能存在一些性能较差的sql语句,平台需要对这种语句进行监控打印,尽快发现平台sql瓶颈进行优化,对此平台采用自定义sql执行时间监控插件方式进行集成。
### 时间监控代码处理
![](https://img.kancloud.cn/ff/db/ffdb8dd23867d5dfd14f75803ed41ac9_2407x1200.png)
### 时间监控自动装配
![](https://img.kancloud.cn/f9/d0/f9d0fcc6c2f3a1377071cb31410623b7_2413x842.png)
## 查询大结果集监控插件
系统运行期间,需要对一些查询大表结果集进行监控,查询是否需要分页分批处理进行优化,防止查询大结果集导致oom风险。
![](https://img.kancloud.cn/36/f7/36f71b5a0c74f0f2033cf276eb5d6f23_2467x1163.png)
## 敏感数据脱敏插件
在某些单位中,安全评测要求十分严格,要求存储到数据库中的数据需要脱敏,同时程序还可以进行like查询,平台采用以下方式进行数据存储脱敏。
![](https://img.kancloud.cn/44/cc/44cce6cb18a9bbea4312b053f902c0e1_563x332.png)
### 使用方法
![](https://img.kancloud.cn/99/28/9928de2b7c4dbdac8f7815f68d144466_2507x1274.png)
## Guava
Guava 还提供了很多实用工具,如 Lists、Maps、Sets,接下来我们分别来看下这些常用工具的使用和原理。
* List list = Lists.newArrayList();
* Map hashMap = Maps.newHashMap();
这种写法其实就是一种简单的工厂模式
~~~
// 可以预估 list 的大小为 20
List<String> list = Lists.newArrayListWithCapacity(20);
List<String> list = Lists.newArrayListWithExpectedSize(20);
Map<String,String> hashMap = Maps.newHashMap();
Map<String,String> linkedHashMap = Maps.newLinkedHashMap();
Map<String,String> withExpectedSizeHashMap = Maps.newHashMapWithExpectedSize(20);
~~~
Guava 还提供了提供了一些异常处理的静态方法
~~~
Throwables.throwIfUnchecked(new RuntimeException("模拟业务出错"));
~~~
## db-spring-boot-starter自动装配原理解析
咱们想想,在不同项目中,咱们的项目是如何使用db-spring-boot-starter装配这些对象的吗?下面咱们需要揭密。
* db-spring-boot-starter 中定义了spring.factories文件
![](https://img.kancloud.cn/bc/24/bc241e3ae9f9228aded77f12e5597711_2427x621.png)
那么这些文件是如何完成加载到spring容器的呢? 此时,咱们必须回到user-center,阅读源码
* @SpringBootApplication
![](https://img.kancloud.cn/3d/b4/3db43ac17272dbaaa221ed7b154e52f1_2437x698.png)
* @EnableAutoConfiguration
![](https://img.kancloud.cn/2d/5a/2d5aa9903abf3e9f44f650a0c20edf72_2399x649.png)
* AutoConfigurationImportSelector
![](https://img.kancloud.cn/f8/14/f81430e1c4fde3772dde30d71d4c2750_2459x573.png)
阅读到这里,我们了解到,user-center在启动时,由于@SpringBootApplication是复合注解,包含@EnableAutoConfiguration,这个类中@import了核心处理类AutoConfigurationImportSelector,这个类的核心就是将classpath中搜索所有META-INF/spring.factories配置文件,并且将其中org.springframework.boot.autoconfigure.EnableAutoConfiguration key对应的配置项加载到spring容器,所以在user-center启动的时候自动装配了db-spring-boot-starter中的配置信息类。
## 总结
通过微内核spi的方式构建db-spring-boot-starter模块提供平台级数据库通用功能。
- 01.前言
- 02.快速开始
- 01.maven构建项目
- 02.安装mysql数据库
- 03.安装redis缓存中间件
- 04.快速启动框架
- 03.总体流程
- 01.架构设计图
- 02.oauth接口
- 03.功能介绍
- 04.部署细节
- 04.模块详解
- 01.基础介绍
- 02.自定义db-spring-boot-starter
- 03.自定义log-spring-boot-starter
- 04.自定义redis-spring-boot-starter
- 05.自定义base-spring-boot-starter
- 06.自定义common-spring-boot-starter
- 07.自定义loadbalancer-spring-boot-starter
- 08.自定义swagger-spring-boot-starter
- 09.自定义uaa-client-spring-boot-starter
- 10.自定义uaa-server-spring-boot-starter
- 11.自定义oss-spring-boot-starter
- 12.自定义sentinel-spring-boot-starter
- 05.服务详解
- 01.nacos-server
- 02.auth-server
- 03.user-center
- 04.new-api-gateway
- 05.file-center
- 06.log-center
- 07.back-center
- 08.auth-sso模块
- 09.admin-server
- 10.job-center
- 06.系统安全
- 01.非法字符漏洞攻击
- 02.防重放攻击
- 03.代码审计
- 04.Xray扫洞
- 05.混沌工程质量保证
- 07.生产部署K8S
- 01.基本环境安装
- 02.基本组件安装
- 03.集群验证
- 04.安装Metrics Server
- 05.安装容器平台
- 06.Ingress网关
- 07.metalb负载均衡器
- 08.容器平台集群
- 08.K8S资源练习
- 01.Deployment
- 02.StatefulSet
- 03.DaemonSet
- 04.redis集群服务
- 05.elasticsearch集群
- 06.rocketmq部署
- 09.生产容器化部署
- 01.nacos集群部署
- 02.user-center服务
- 03.auth-server服务
- 04.new-api-gateway服务
- 技术交流