在實際工作中我們我們使用較多的則是Spring預設的HikariDataSource資料庫連線池,但是它無法提供視覺化監控SQL這一能力,而這在很多場景下往往又是我們需要的功能,因此今天來學習阿里開源的一款優秀的資料庫連線池---Druid。Druid能夠提供強大的SQL監控和功能擴充套件能力,允許開發者根據需要進行二次開發。
首先我們使用傳統的方式,快速搭建一個具備查詢使用者資訊的簡單專案。
第一步,新建一個名為druid-sql
的SpringBoot專案,選擇spring web
、mybatis framework
和mysql driver
依賴:
第二步,修改application.properties
組態檔資訊:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
第三步,建立book
這一資料表:
USE druid_sql;
DROP TABLE IF EXISTS book;
CREATE TABLE book (
id INT auto_increment PRIMARY KEY,
name VARCHAR ( 255 ),
price INT,
description VARCHAR ( 500 )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
第四步,建立Book實體類:
public class Book {
private Integer id;
private String name;
private Integer price;
private String description;
//省略getter和setter方法
}
第五步,建立BookMapper的介面檔案:
@Mapper
public interface BookMapper {
List<Book> selectBookByName(String name);
}
第六步,建立BookMapper的XML檔案:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.melody.druidsql.mapper.BookMapper">
<select id="selectBookByName" resultType="com.melody.druidsql.entity.Book">
select * from book where name=#{name}
</select>
</mapper>
第七步,新建BookService類:
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;
public List<Book> selectBookByName(String name){
return bookMapper.selectBookByName(name);
}
}
第八步,新建BookController類:
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/book")
public List<Book> selectBookByName(String name){
return bookService.selectBookByName(name);
}
}
第九步,新增資料進行測試,這些都是比較常規的操作了。通過檢視原始碼,可以發現它使用的是HikariDataSource資料庫連線池:
第一步,在POM檔案中新增如下依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
第二步,修改application.properties
組態檔資訊。首先設定WebStatFilter,它用於採集web-jdbc
所關聯的監控資料:
# Druid相關設定
# 開啟WebStatFilter
spring.datasource.druid.web-stat-filter.enabled=true
# 設定攔截規則,這裡設定所有
spring.datasource.druid.web-stat-filter.url-pattern=/*
# 排除一些不會涉及到 SQL 查詢的 URL
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
# 開啟 session 統計功能
spring.datasource.druid.web-stat-filter.session-stat-enable=true
# 預設 sessionStatMaxCount 值為 1000 ,開發者可按需進行設定
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
其次,設定StatViewServlet相關設定項,如下所示:
# 設定StatViewServlet
# 啟用內建的監控頁面
spring.datasource.druid.stat-view-servlet.enabled=true
# 內建監控頁面的 URL 地址
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
# 開啟 Reset All 功能,注意值設定為false,按鈕會顯示,但是點選沒有反應
spring.datasource.druid.stat-view-servlet.reset-enable=true
# 設定監控頁面登入使用者名稱
spring.datasource.druid.stat-view-servlet.login-username=melody
# 設定監控頁面登入密碼
spring.datasource.druid.stat-view-servlet.login-password=melody
# 設定白名單(如果allow沒有設定或者為空,表示允許所有存取)
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
# 黑名單(注意 deny 的優先順序高於 allow,即只有在 deny 列表中,那麼即使在 allow 列表中,也會被拒絕)
spring.datasource.druid.stat-view-servlet.deny=
最後設定過濾器, stat 過濾器用於監控 SQL 執行:
# 過濾器設定
spring.datasource.druid.filters=stat
第三步,啟動專案進行測試。首先存取http://localhost:8080/druid/login.html
連結,頁面如下所示:
之後輸入剛才設定的melody/melody資訊進行登入,登入介面如下所示:
可以看到Druid自帶了很多功能,如:資料來源、SQL監控、SQL防火牆、Web應用、URI監控、Session監控、Spring監控和JSON API等。
先不管這些功能,我們先嚐試存取一下如下連結:
http://localhost:8080/book?name=java
可以看到頁面已經可以顯示資料了:
再看一下SQL監控,這裡也將我們剛才呼叫介面執行SQL的執行過程給記錄了:
以上就是Druid的初體驗了,接下來我們來分析Druid首頁導航欄上的各個模組。
Druid連線池是阿里巴巴開源的資料庫連線池專案。Druid連線池為監控而生,內建強大的監控功能,監控特性不影響效能。功能強大,能防SQL隱碼攻擊,內建Loging能診斷Hack應用行為。
可以看出,Druid連線池在效能、監控、診斷、安全、擴充套件性這些方面遠遠超出競品。
對於連線池來說,連線池本身的效能消耗在整個呼叫鏈路中通常佔比不大,連線池的效能關鍵點是,連線是否LRU的方式重用,是否支援PSCache(PreparedStatementCache)
才是效能的關鍵點。當然DruidDataSource在沒有使用Filter沒有開啟testOnBorrow的情況下,裸測也是極好。
Druid連線池最初就是為監控系統採集jdbc執行資訊而生的,它內建了StatFilter功能,能採集非常完備的連線池執行資訊。
Druid連線池內建了能和Spring/Servlet關聯監控的實現,使得監控Web應用特別方便。
Druid連線池內建了一個監控頁面,提供了非常完備的監控資訊,可以快速診斷系統的瓶頸。
Druid連線池的監控資訊主要是通過StatFilter採集的,採集的資訊非常全面,包括SQL執行、並行、慢查、執行時間區間分佈等。
Druid增加StatFilter之後,能採集大量統計資訊,同時對效能基本沒有影響。StatFilter對CPU和記憶體的消耗都極小,對系統的影響可以忽略不計。監控不影響效能是Druid連線池的重要特性。
實際業務中,如果SQL不是走PreparedStatement
,SQL沒有引數化,這時SQL需要引數化合並監控才能真實反映業務情況。如下SQL:
select * from t where id = 1
select * from t where id = 2
select * from t where id = 3
引數化後:
select * from t where id = ?
引數化合並監控是基於SQL Parser
語法解析實現的,是Druid連線池獨一無二的功能。
StatFilter能採集到每個SQL的執行次數、返回行數總和、更新行數總和、執行中次數和和最大並行。並行監控的統計是在SQL執行開始對計數器加一,結束後對計數器減一實現的。可以採集到每個SQL的當前並行和採集期間的最大並行。
預設執行耗時超過3秒的被認為是慢查,統計項中有包括每個SQL的最後發生的慢查的耗時和發生時的引數。
如果SQL執行時丟擲了Exception,SQL統計項上會Exception有最後的發生時間、堆疊和Message,根據這些資訊可以很容易定位錯誤原因。
SQL監控項上,執行時間、讀取行數、更新行數都有區間分佈,將耗時分佈成8個區間:
記錄耗時區間的發生次數,通過區間分佈,可以很方便看出SQL執行的極好、普通和極差的分佈。 耗時區分分佈提供了「執行+RS時分佈」,是將執行時間+ResultSet持有時間合併監控,這個能方便診斷返回行數過多的查詢。
Druid連線池內建了LogFilter,將Connection/Statement/ResultSet相關操作的紀錄檔輸出,可以用於診斷系統問題,也可以用於Hack一個不熟悉的系統。
LogFilter可以輸出連線申請/釋放,事務提交回滾,Statement的Create/Prepare/Execute/Close
,ResultSet的Open/Next/Close
,通過LogFilter可以詳細診斷一個系統的Jdbc行為。同時LogFilter有Log4j、Log4j2、Slf4j、CommsLog等實現。
SQL隱碼攻擊是駭客對資料庫進行攻擊的常用手段,Druid連線池內建了WallFilter提供防SQL隱碼攻擊功能,在不影響效能的同時防禦SQL隱碼攻擊。
Druid連線池內建了一個功能完備的SQL Parser,能夠完整解析mysql、sql server、oracle、postgresql的語法,通過語意分析能夠精確識別SQL隱碼攻擊。
基於SQL語意分析,大量應用和反饋,使得Druid的防SQL隱碼攻擊擁有極低的漏報率和誤報率。
內建引數化後的Cache、高效能手寫的Parser,使得開啟防SQL隱碼攻擊對應用的效能基本不受影響。
更多Druid的詳細介紹,可以參考 這裡 ,瞭解更多。
Druid內建提供了一個StatViewServlet用於展示Druid的統計資訊,這個StatViewServlet的用途包括:(1)提供監控資訊展示的html頁面;(2)提供監控資訊的JSON API。
首先我們檢視一下這個StatViewServlet類的資訊,可以發現它是一個靜態內部類:
public static class StatViewServlet {
private boolean enabled;
private String urlPattern;
private String allow;
private String deny;
private String loginUsername;
private String loginPassword;
private String resetEnable;
//setter和setter方法
}
也就是說,如果我們要設定StatViewServlet,啟用內建的監控頁面,首先需要在application.properties
檔案中新增如下設定:
spring.datasource.druid.stat-view-servlet.enabled=true
其次設定監控頁面的存取URL地址,可通過urlPattern屬性設定,如下設定時的內建監控頁面的首頁是/druid/index.html
:
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
開發者可以通過loginUsername和loginPassword屬性來設定登入資訊:
spring.datasource.druid.stat-view-servlet.login-username=melody
spring.datasource.druid.stat-view-servlet.login-password=melody
由於StatViewSerlvet展示出來的監控資訊是系統執行的內部情況,因此資料比較敏感,如果開發者想要實現存取控制,可以設定allow和deny這兩個屬性。
allow用於設定白名單(如果allow沒有設定或者為空,表示允許所有存取),而deny用於設定黑名單,注意 deny 的優先順序高於 allow,如果在deny列表中,就算在allow列表中,也會被拒絕:
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
spring.datasource.druid.stat-view-servlet.deny=
在StatViewSerlvet輸出的html頁面中,有一個功能是Reset All,即執行這個操作後,會清零所有計數器,並重新計數。開發者你可通過resetEnable屬性來實現該功能的啟用與否:
spring.datasource.druid.stat-view-servlet.reset-enable=true
如果開發者想要設定Web關聯監控,那麼可以檢視後面關於 URI監控 模組內容;設定Spring關聯監控,那麼可以檢視後面關於 Web應用 模組內容。
更多詳細設定項,可以參考 這裡 ,瞭解更多。
資料來源詳細記錄了當前專案所使用的資料來源資訊,如登入使用者名稱、地址、資料庫型別、驅動型別、filter類名、連線設定、事務設定等內容:
DruidDataSource
是資料來源屬性設定類,檢視一下該類的原始碼:
public class DruidDataSource extends DruidAbstractDataSource implements DruidDataSourceMBean, ManagedDataSource, Referenceable, Closeable, Cloneable, ConnectionPoolDataSource, MBeanRegistration {
private static final Log LOG = LogFactory.getLog(DruidDataSource.class);
private static final long serialVersionUID = 1L;
private volatile long recycleErrorCount;
private long connectCount;
private long closeCount;
private volatile long connectErrorCount;
private long recycleCount;
......
}
可以看到該類繼承自DruidAbstractDataSource類,這個類是資料來源屬性的抽象類,之所以將資料來源定義為抽象類是為了讓其他子類可以在此基礎上進行擴充套件,因為DruidDataSource大部分屬性都是參考DBCP的:
一般來說,開發者只需設定url(並不是此處看到的jdbcUrl),username、password和max-active這四項:
spring.datasource.druid.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
spring.datasource.druid.username=root
spring.datasource.druid.password=root
spring.datasource.druid.max-active=20
當然了,Druid也會根據URL來自動識別驅動類名稱,無須開發者手動新增。舉個例子,如果使用的是常見資料庫如MySQL,可以使用舊的連線資訊:
spring.datasource.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
不需要在加一層druid字首,這一點還是很方便的。如果連線的資料庫是非常見資料庫,那麼必須新增driverClassName屬性。
更多詳細設定項,可以參考 這裡 ,瞭解更多。
請注意,要想使用SQL監控,首先必須設定對應的過濾器,這些過濾器都需要實現com.alibaba.druid.filter
介面,可以看到它也有很多子類:
由於此處是監控SQL,因此需要設定StatFilter,即在application.properties
檔案中新增如下設定:
spring.datasource.druid.filters=stat
如果你想使用後續的SQL防火牆,就需要使用wall這一設定項,自然想使用log4j2,就設定這一項,這些在資料來源模組的Filter類名中就可以得到體現:
SQL監控模組主要用於監控SQL,並展示SQL的執行數、執行時間、最慢、事務執行、錯誤數、更新行數、讀取行數、執行中、最大並行、執行時間分佈、執行+RS時分佈、讀取行分佈和更新行分佈等資訊,其實這就是讓開發者對整個專案中執行的SQL都有一個全域性的瞭解:
StatFilter的別名是stat,這個別名對映設定資訊儲存在druid-xxx.jar!/META-INF/druid-filter.properties
檔案中。
更多詳細設定項,可以參考 這裡 ,瞭解更多。
在druid-xxx.jar!/META-INF/druid-filter.properties
檔案中設定了如下Filter的別名,因為我們是通過對應Filter的類名來設定對應的Filter別名,多個Filter可以組合使用:
Filter的類名 | Filter別名 |
---|---|
default | com.alibaba.druid.filter.stat.StatFilter |
stat | com.alibaba.druid.filter.stat.StatFilter |
mergeStat | com.alibaba.druid.filter.stat.MergeStatFilter |
counter | com.alibaba.druid.filter.stat.StatFilter |
encoding | com.alibaba.druid.filter.encoding.EncodingConvertFilter |
log4j | com.alibaba.druid.filter.logging.Log4jFilter |
log4j2 | com.alibaba.druid.filter.logging.Log4j2Filter |
slf4j | com.alibaba.druid.filter.logging.Slf4jLogFilter |
commonlogging | com.alibaba.druid.filter.logging.CommonsLogFilter |
commonLogging | com.alibaba.druid.filter.logging.CommonsLogFilter |
wall | com.alibaba.druid.wall.WallFilter |
config | com.alibaba.druid.filter.config.ConfigFilter |
haRandomValidator | com.alibaba.druid.pool.ha.selector.RandomDataSourceValidateFilter |
WallFilter用於實現SQL防火牆,首先我們檢視一下這個WallFilter類的資訊,可以發現它是一個類:
public class WallFilter extends FilterAdapter implements WallFilterMBean {
private static final Log LOG = LogFactory.getLog(WallFilter.class);
private boolean inited = false;
private WallProvider provider;
private String dbTypeName;
private WallConfig config;
private volatile boolean logViolation = false;
private volatile boolean throwException = true;
public static final String ATTR_SQL_STAT = "wall.sqlStat";
public static final String ATTR_UPDATE_CHECK_ITEMS = "wall.updateCheckItems";
private static final ThreadLocal<List<Integer>> tenantColumnsLocal = new ThreadLocal();
......
}
如果開發者想要啟用這個WallFilter,那麼需要在application.properties
組態檔中通過Filter類名來進行設定:
spring.datasource.druid.filters=wall
當然了,還可以結合其他Filter一起使用,如下:
spring.datasource.druid.filters=wall,stat
但是這樣設定會造成攔截檢測的時間不在StatFilter統計的SQL執行時間內,所以如果希望攔截檢測的時間在StatFilter統計的SQL執行時間內,需要調整兩者的執行順序:
spring.datasource.druid.filters=stat,wall
SQL防火牆主要分為5大部分:防禦統計、表存取統計、函數呼叫統計、SQL防禦統計的黑白名單。
更多詳細設定項,可以參考 這裡 ,瞭解更多。
它可以展示當前應用中的相關資訊,如ContextPath、最大並行、請求次數、Jdbc執行數、讀取行數和更新行數等一系列資訊。
WebStatFilter用於採集web-jdbc
關聯監控的資料,首先我們檢視一下這個WebStatFilter類的資訊,可以發現它是一個靜態內部類:
public static class WebStatFilter {
private boolean enabled;
private String urlPattern;
private String exclusions;
private String sessionStatMaxCount;
private String sessionStatEnable;
private String principalSessionName;
private String principalCookieName;
private String profileEnable;
//setter和setter方法
}
因此我們首先需要在application.properties
組態檔中開啟WebStatFilter:
spring.datasource.druid.web-stat-filter.enabled=true
如果開發者想要針對部分URL進行攔截,那麼可以使用如下設定,此處攔截所有,即所有的API存取都會被記錄:
spring.datasource.druid.web-stat-filter.url-pattern=/*
當然了有些不會涉及到 SQL 查詢的API,我們是希望可以排除掉,那麼可以設定如下:
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
當我們需要開啟session統計功能時,可以使用如下設定項:
spring.datasource.druid.web-stat-filter.session-stat-enable=true
想要設定sessionStatMaxCount的值時,可以使用如下設定項,該值預設為1000:
spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
如果開發者想讓Druid知道當前的session的使用者是誰,可以設定principalSessionName
,注意這個引數的值是user資訊儲存在session中的 sessionName
,如果你session中儲存的是非string型別的物件,那麼需要過載toString方法:
spring.datasource.druid.web-stat-filter.principal-session-name=
如果你的user資訊是存在cookie中,那麼可以使用下面的方式同樣可以設定principalSessionName
,進而也能讓Druid知道當前的session的使用者是誰:
spring.datasource.druid.web-stat-filter.principal-cookie-name=
自Druid0.2.7版本開始就支援profile,設定profileEnable就能夠監控單個url呼叫的sql列表:
spring.datasource.druid.web-stat-filter.profile-enable=
可以開啟一下profile試試,然後多次呼叫book這一API,結果如下所示:
更多詳細設定項,可以參考 這裡 ,瞭解更多。
Druid提供了Spring和Jdbc的關聯監控,在使用的時候需要先閱讀檔案然後進行相應的設定,可點選 這裡 進行閱讀。
如果開發者覺得此處展示的資料不太美觀,那麼可以根據提供的API來獲取資訊後自行展示。
一般來說為了支援開源,不建議隨便把廣告去掉,但是在企業裡面這個廣告實在是有損形象,所以考慮再三還是決定去掉廣告。
首先F12檢視原始碼:
然後分析發現廣告是由commons.js
檔案帶出來的,該檔案存放於ruid-xxx.jar!/support/http/resources/js/common.js
檔案中,可以存取http://localhost:8080/druid/js/common.js
連結進行確認:
再來檢視一下該網頁的原始碼:
呼叫的是init方法,再閱讀一下原始碼:
init : function() {
this.buildFooter();
druid.lang.init();
},
buildFooter : function() {
var html ='';
$(document.body).append(html);
},,
確認一下,也就說這個buildFooter方法用於顯示頁面底部的廣告,而這個方法則是在init方法中呼叫的:
所以要想去除廣告,不呼叫這個buildFooter方法即可,但是原始碼這種除非反編譯,否則是無法修改的。不過可以換種實現方式,可以寫一個過濾器,先過濾對於commons.js
檔案的請求,之後再讀取commons.js
檔案內容,並將this.buildFooter();
這行程式碼用空字串取代,最後再將這個檔案返回就行。
定義一個filter包,並在裡面定義一個DeleteADFilter類需要實現Filter介面並重寫其中的doFilter方法:
@WebFilter(urlPatterns ="/druid/js/common.js")
public class DeleteADFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletResponse.resetBuffer();
String content = Utils.readFromResource("support/http/resources/js/common.js");
content = content.replace("this.buildFooter();","");
servletResponse.getWriter().write(content);
}
}
注意過濾的url地址必須和原地址保持一致,然後在專案啟動類上掃描一下我們新增的DeleteADFilter類:
@SpringBootApplication
@ServletComponentScan("com.melody.druidsql.filter")
public class DruidSqlApplication {
public static void main(String[] args) {
SpringApplication.run(DruidSqlApplication.class, args);
}
}
之後重新啟動專案,存取首頁或者其他頁面,可以發現廣告已經去掉了:
注意不能將專案啟動類上的@ServletComponentScan("com.melody.druidsql.filter")
註解去掉,轉而在DeleteADFilter類上新增@Component
,因為前者是註冊,後者則是掃描,作用不同。相反如果你使用了@ServletComponentScan
註解,那麼Servlet可以直接通過@WebServlet
註解自動註冊;Filter可以直接通過@WebFilter
註解自動註冊;Listener可以直接通過@WebListener
註解自動註冊。
當然了除了上面的手動註冊方式外,我們還可以使用自動註冊的方式。只需定義一個DruidSqlConfiguration
類,然後提供一個FilterRegistrationBean
範例即可,我們可以在該方法中書寫攔截廣告的邏輯:
@Configuration
public class DruidSqlConfiguration {
@Bean
@ConditionalOnProperty(name = {"spring.datasource.druid.stat-view-servlet.enabled"},havingValue = "true")
public FilterRegistrationBean removeAdFilterRegistrationBean(DruidStatProperties druidStatProperties){
//獲取Web監控頁面的引數
DruidStatProperties.StatViewServlet statViewServlet = druidStatProperties.getStatViewServlet();
//提取common.js的設定路徑
String urlPattern = statViewServlet.getUrlPattern() != null? statViewServlet.getUrlPattern():"/druid/*";
String commonJsPattern = urlPattern.replaceAll("\\*","js/common.js");
//定義過濾器
Filter filter = new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String content = Utils.readFromResource("support/http/resources/js/common.js");
content = content.replace("this.buildFooter();","");
servletResponse.getWriter().write(content);
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}
可以看到這種方式相比於前面那種更為巧妙,尤其是在當還需要往Spring容器主註冊其他Bean的時候。
當然了,還有許多功能,如資料庫多資料來源、設定資料庫加密、儲存Druid的監控記錄等等,這些都將會在後續文章中進行介紹。
歡迎關注微信公眾號「啃餅思錄」,博主等你來撩!