版权声明:本文为博客园博主「水木桶」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://www.cnblogs.com/shuimutong/p/11408219.html

背景

手写DAO框架系列前后更新了5篇文章,外加1篇使用示例,GDAO框架至此已经初具雏形,简单的使用不成问题。接下来就是对框架进行不断的优化。

顺便说一下性能

手写DAO框架(六)-后续之框架使用示例 此篇文章对GDAO的性能较少提及,这里就简单的记载一下,后续有机会再更新。

数据示例:id:14005  name:BatchName-4009  age:52,其中id是自动生成。

数据库是mariadb,和测试程序位于同一台机器

测试数据条数5000条,一次调用

userDao.add(uds[i]);

方法插入一条数据。

系统 配置 结果
macOS 2c4t,8g,固态硬盘 总耗时:8629.0ms,平均耗时:1.726ms
Win10 6c6t,24g,固态硬盘 总耗时:5153.0ms,平均耗时:1.031ms

一、当前问题分析

对于一个提供连接池功能的DAO框架,如果保存的连接失效了无法自动移除池,如果连接数据库的网络出现闪断连接无法继续使用,只能通过重启服务来达到初始化连接的目的,这样的做法显然是不够优雅的。

为了提高框架的稳定性,所以决定对框架的连接部分做一次优化。

二、需求整理

通过网上查资料,拟定了几个点。

1、自动重连

autoReconnect=true

JDBC通过配置可实现

2、连接有效性检测

a、配置连接检测语句。备注:有的数据库Driver支持ping(),可以使用。

3、连接泄露检查

当连接从连接池借出后,长时间(配置时间)不归还,将强制回收。

4、了解到的其他问题

如果连接闲置时间过长,可能被mysql主动关闭。

正常的使用-归还(连接放到队列,队列是先进先出,下次再取,形成一个循环)流程,可避免连接闲置时间过长,暂缓优化。

优化点确定

2、连接有效性检测

3、连接泄露检查

三、编码实现

博主开始写代码了,需要一段时间

连接有效性检测,根据配置来检测,代码参考MysqlValidConnectionChecker。

连接泄露检查,通过启一条线程,根据配置时间进行检查。

----------------------经过了好几天的编写,代码完成了---------------------------------------------

直接上代码。

1、增加配置

#########v03##########
#连接检测语句
checkConnectionValidationQuery=select 1
#归还连接时检测连接,true false
checkConnectionWhenReturn=true
#定时检测连接间隔时长(分钟)
periodCheckConnectionTimeMin=10
#连接泄露检测
connectionLeakCheck=true
#连接泄露检测间隔时长(分钟)
connectionLeakCheckPeriodTimeMin=10
#强制归还连接时长(小时)
forceReturnConnectionTimeHour=6

说明:

1)配置可以分为1个基础,3个部分。

1个基础是指连接检测语句,3个部分分别对应归还连接时检测连接、定时检测连接间隔时长、连接泄露检测

2)定时检测主要是防止连接中断了不能自动生成新的连接,连接间隔时长如果设为0,则不会定时检测。

2、主要代码

 package me.lovegao.gdao.connection;

 import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import me.lovegao.gdao.bean.SystemConstant;
import me.lovegao.gdao.util.ConnectionUtil; /**
* 第二版简易连接池实现<br/>
* 主要增加连接有效性检测,包括归还连接时检测,定时检测连接
*
* @author simple
*
*/
public class SimpleV2ConnectionPool extends SimpleConnectionPool {
private final static Logger log = LoggerFactory.getLogger(SimpleV2ConnectionPool.class);
private ExecutorService ES;
// 归还连接时检测连接,这步最好做成异步的,避免影响归还速度
private boolean checkConnectionWhenReturn = false;
// 连接检测语句
private String checkConnectionValidationQuery;
// 定时检测连接的时间(分钟)
private int periodCheckConnectionTimeMin;
/** 待检测连接 **/
private volatile Queue<Connection> TO_CHECK_CONNECTION_POOL;
//查询超时时间
private final int QUERY_TIMEOUT_SECONDS;
//连接泄露检测
private boolean checkConnectionLeak = false;
//连接泄露检测间隔时长-分钟
private int checkConnectionLeakPeriodTimeMin = 30;
//强制归还连接时长(小时)
private double forceReturnConnectionTimeHour;
//连接最大空闲时长(小时)
// private double connectionMaxIdleTimeHour;
/**连接最后借出时间**/
private Map<Integer, Long> CONNECTION_OUT_TIME_MAP_POOL = null; public SimpleV2ConnectionPool(Properties properties) throws Exception {
super(properties);
QUERY_TIMEOUT_SECONDS = super.getQueryTimeoutSecond();
initProp(properties);
initCheck();
} private void initProp(Properties properties) {
//连接有效性检测配置
if (properties.containsKey(SystemConstant.STR_CHECK_CONNECTION_VALIDATION_QUERY)) {
checkConnectionValidationQuery = properties
.getProperty(SystemConstant.STR_CHECK_CONNECTION_VALIDATION_QUERY);
String checkWhenReturn = properties.getProperty(SystemConstant.STR_CHECK_CONNECTION_WHEN_RETURN);
if (checkWhenReturn.toLowerCase().equals("true")) {
checkConnectionWhenReturn = true;
}
if (properties.containsKey(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN)) {
String periodTimeStr = properties.getProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN);
if (StringUtils.isNumeric(periodTimeStr)) {
periodCheckConnectionTimeMin = Integer.parseInt(periodTimeStr);
}
}
}
//连接泄露检测配置
if (properties.containsKey(SystemConstant.STR_CONNECTION_LEAK_CHECK)) {
String leakCheckStr = properties.getProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK);
if (leakCheckStr.toLowerCase().equals("true")) {
String leakCheckPeriodTimeStr = properties.getProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK_PERIOD_TIME_MIN);
if (StringUtils.isNumeric(leakCheckPeriodTimeStr)) {
checkConnectionLeakPeriodTimeMin = Integer.parseInt(leakCheckPeriodTimeStr);
}
String forceReturnTimeStr = properties.getProperty(SystemConstant.STR_FORCE_RETURN_CONNECTION_TIME_HOUR);
if (NumberUtils.isNumber(forceReturnTimeStr)) {
forceReturnConnectionTimeHour = Double.parseDouble(forceReturnTimeStr);
if(forceReturnConnectionTimeHour > 0) {
checkConnectionLeak = true;
}
}
//最大空闲检测,功能上和定时检测连接重复,暂时不开发。 //需要同时配置(强制归还时间)才能检测连接泄露
if(forceReturnConnectionTimeHour > 0) {
checkConnectionLeak = true;
}
}
}
StringBuilder infoSb = new StringBuilder();
infoSb.append("SimpleV2ConnectionPoolInitDone------")
.append(",checkConnectionValidationQuery:").append(checkConnectionValidationQuery)
.append(",checkConnectionWhenReturn:").append(checkConnectionWhenReturn)
.append(",periodCheckConnectionTimeMin:").append(periodCheckConnectionTimeMin)
.append(",checkConnectionLeak:").append(checkConnectionLeak)
.append(",checkConnectionLeakPeriodTimeMin:").append(checkConnectionLeakPeriodTimeMin)
.append(",forceReturnConnectionTimeHour:").append(forceReturnConnectionTimeHour);
System.out.println(infoSb.toString());
} @Override
public Connection getConnection() throws Exception {
Connection conn = super.getConnection();
if(checkConnectionLeak) {
int connHashCode = conn.hashCode();
CONNECTION_OUT_TIME_MAP_POOL.put(connHashCode, System.currentTimeMillis());
}
return conn;
} @Override
public void returnConnection(Connection conn) {
//检测连接泄露
if(checkConnectionLeak) {
int connHashCode = conn.hashCode();
//连接超时过长,已经被主动移除
if(!CONNECTION_OUT_TIME_MAP_POOL.containsKey(connHashCode)) {
return;
} else {
CONNECTION_OUT_TIME_MAP_POOL.remove(connHashCode);
}
}
//检测归还连接
if (checkConnectionWhenReturn) {
if(TO_CHECK_CONNECTION_POOL.isEmpty()) {
synchronized(TO_CHECK_CONNECTION_POOL) {
if(TO_CHECK_CONNECTION_POOL.isEmpty()) {
TO_CHECK_CONNECTION_POOL.add(conn);
TO_CHECK_CONNECTION_POOL.notifyAll();
} else {
TO_CHECK_CONNECTION_POOL.add(conn);
}
}
} else {
TO_CHECK_CONNECTION_POOL.add(conn);
}
} else {
superReturnConnection(conn);
}
} @Override
public void closeConnectionPool() {
if(ES != null) {
ES.shutdownNow();
}
super.closeConnectionPool();
} private void superReturnConnection(Connection conn) {
super.returnConnection(conn);
} private Connection superGetByConnectionHashCode(int hashCode) {
return super.getByConnectionHashCode(hashCode);
} // 初始化检查
private void initCheck() {
int threadPoolSize = 0;
if(checkConnectionWhenReturn) {
threadPoolSize += 2;
TO_CHECK_CONNECTION_POOL = new ConcurrentLinkedQueue();
}
if(periodCheckConnectionTimeMin > 0) {
threadPoolSize++;
}
//需要同时配置(强制归还时间、最大空闲时间)才能检测连接泄露
if(checkConnectionLeak) {
threadPoolSize++;
CONNECTION_OUT_TIME_MAP_POOL = new ConcurrentHashMap();
}
if(threadPoolSize > 0) {
ES = Executors.newFixedThreadPool(threadPoolSize);
// 检查归还连接
if(checkConnectionWhenReturn) {
//启两个线程同时检测
for(int i=0; i<2; i++) {
ES.execute(new ReturnConnectionCheck());
}
}
//定时检测连接
if (periodCheckConnectionTimeMin > 0) {
ES.execute(new ConnectionPeriodCheck());
}
//连接泄露检测
if(checkConnectionLeak) {
ES.execute(new ConnectionLeakCheck());
}
}
} /**
* 连接泄露定时检测
* @author simple
*
*/
class ConnectionLeakCheck implements Runnable {
int sleepTimeMs = checkConnectionLeakPeriodTimeMin * 60 * 1000;
@Override
public void run() {
Set<Integer> preConnHashCodeSet = new HashSet();
while (true) {
if(ES.isShutdown()) {
break;
}
try {
Thread.sleep(sleepTimeMs);
} catch (Exception e) {
log.error("ConnectionLeakCheckSleepException", e);
}
try {
checkConnectionLeak(preConnHashCodeSet);
} catch (Exception e) {
log.error("ConnectionLeakCheckException", e);
}
}
}
} //检测连接泄露
private void checkConnectionLeak(Set<Integer> preConnHashCodeSet) throws Exception {
if(CONNECTION_OUT_TIME_MAP_POOL.size() < 1) {
preConnHashCodeSet = new HashSet();
} else {
Iterator<Entry<Integer, Long>> connHashCodeIt = CONNECTION_OUT_TIME_MAP_POOL.entrySet().iterator();
//先对比前后两次的连接,如果有相同的,再检测相同的连接
if(preConnHashCodeSet.size() == 0) {
while(connHashCodeIt.hasNext()) {
preConnHashCodeSet.add(connHashCodeIt.next().getKey());
}
} else {
StringBuilder logSb = new StringBuilder();
long timeFlag = (long) (System.currentTimeMillis() - forceReturnConnectionTimeHour * 3600 * 1000);
logSb.append("ConnectionLeakCheck---")
.append(",timeFlag:").append(timeFlag)
.append(",forceReturnConnectionTimeHour:").append(forceReturnConnectionTimeHour);
List<Integer> toCloseConnectionHashCodeList = new ArrayList();
logSb.append(",toCloseConn,{");
//过滤出两次集合重合,且已经超时的元素
while(connHashCodeIt.hasNext()) {
Entry<Integer, Long> connEntry = connHashCodeIt.next();
int connHashCode = connEntry.getKey();
if(preConnHashCodeSet.contains(connHashCode) && connEntry.getValue() < timeFlag) {
toCloseConnectionHashCodeList.add(connHashCode);
logSb.append(connHashCode).append(":").append(connEntry.getValue()).append(",");
}
}
logSb.append("}");
if(toCloseConnectionHashCodeList.size() > 0) {
for(Integer connHashCode : toCloseConnectionHashCodeList) {
Connection conn = superGetByConnectionHashCode(connHashCode);
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
log.error("closeConnectionException", e);
}
CONNECTION_OUT_TIME_MAP_POOL.remove(connHashCode);
superReturnConnection(conn);
}
}
}
log.info(logSb.toString());
//进行过一次检测之后,对之前存储的进行初始化
preConnHashCodeSet = new HashSet();
}
}
} /**
* 归还连接检测
* @author simple
*
*/
class ReturnConnectionCheck implements Runnable {
@Override
public void run() {
while (true) {
if(ES.isShutdown()) {
break;
}
Connection toCheckConn = TO_CHECK_CONNECTION_POOL.poll();
if (toCheckConn == null) {
try {
synchronized(TO_CHECK_CONNECTION_POOL) {
TO_CHECK_CONNECTION_POOL.wait();
}
} catch (InterruptedException e) {
log.error("checkReturnConnectionWaitException", e);
}
} else {
boolean canUse = ConnectionUtil.isValidConnection(toCheckConn, checkConnectionValidationQuery,
QUERY_TIMEOUT_SECONDS);
if (!canUse) {
try {
toCheckConn.close();
} catch (SQLException e) {
log.error("checkReturnConnectionCloseConnException", e);
}
}
superReturnConnection(toCheckConn);
}
}
}
} /**
* 连接定时检测
* @author simple
*
*/
class ConnectionPeriodCheck implements Runnable {
int sleepTimeMs = periodCheckConnectionTimeMin * 60 * 1000;
@Override
public void run() {
while (true) {
if(ES.isShutdown()) {
break;
}
try {
Thread.sleep(sleepTimeMs);
} catch (Exception e) {
log.error("checkReturnConnectionSleepException", e);
}
while(true) {
//是否继续检测
boolean continueCheck = false;
Connection toCheckConn = null;
try {
toCheckConn = getConnection();
if (toCheckConn != null) {
boolean canUse = ConnectionUtil.isValidConnection(toCheckConn,
checkConnectionValidationQuery, QUERY_TIMEOUT_SECONDS);
if (!canUse) {
toCheckConn.close();
//连接不可用,继续检测其他连接是否正常
continueCheck = true;
log.info("oneConnectionCannotUse,closeIt.....");
}
}
} catch (Exception e) {
log.error("checkReturnConnectionException", e);
continueCheck = true;
} finally {
if (toCheckConn != null) {
superReturnConnection(toCheckConn);
}
}
if(continueCheck) {
log.info("checkOneConnectionCannotUse,beginToCheckOtherConnection....");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
log.error("checkReturnConnectionWaitException", e);
}
} else {
break;
}
}
}
}
}
}

3、主要思路

1)归还连接时检测连接的思路

归还连接的时候,如果不采用异步,那么归还连接的线程必须等待连接确认完毕之后才能继续执行,这样做感觉性能不是最优的。

所以引入了异步,归还连接时,连接直接放到一个待检测的容器里,不需要等待检测完之后再返回。

待检测连接由检测线程异步进行检测。检测现场从待检测容器里取连接进行检测,必然会出现空的情况。

出现了空的情况怎么做好呢,是在那里自旋等待?是休眠一段时间再检测?还是等待呢?

自旋等待,浪费计算资源;休眠的话,休眠时长不好确定,谁知道下一毫秒会发生什么?万一因为连接未及时检测出现了连接用尽,岂不是很尴尬?

所以,我选择了等待,当线程归还时,主动唤醒等待线程。

代码实现之后,测试的时候,我发现运行报错。wait()、notifyAll()方法不是那样用的,需要获取锁。

添加了锁之后,为了尽量减少同步带来的性能损失,我采取了写单例时经常提到的双重检查:我不需要每次都要拿锁、通知,只需要在待检测连接池是空的时候才需要进行拿锁、通知。、

2)连接泄露检测的思路

连接泄露检测,主要是为了防止连接被借出去之后,很久都没有归还的情景。

很久不归还,这个连接还占着连接池的坑,却没法被复用,所以需要进行检测。

检测需要确定的就是,取出多久算久?然后就是,多久检测一次?

具体实现的时候,还有一个问题,就是强制归还的连接应该怎么归还?直接放回连接池吗?万一连接真的还在被别的线程使用怎么办?

所以这里,我采取先把连接关闭了,然后再归还。

3)定时检测连接的思路

为了保持连接池的连接都是最终可用的,所以需要对连接池的连接进行定时的检测。

如果连接不可用,就把连接关闭,然后从连接池去除。

4、测试代码

1)配置

##驱动名称
driverName=com.mysql.jdbc.Driver
##连接url
connectionUrl=jdbc:mysql://localhost:3306/simple?autoReconnect=true&useServerPrepStmts=false&rewriteBatchedStatements=true&connectTimeout=1000&useUnicode=true&characterEncoding=utf-8
##用户名
userName=simple
##用户密码
userPassword=123456
##初始化连接数
initConnectionNum=10
##最大连接数
maxConnectionNum=50
##最大查询等待时间
maxQueryTime=3
#########v03##########
#归还连接时检测连接,true false
checkConnectionWhenReturn=true
#连接检测语句
checkConnectionValidationQuery=select 1
#定时检测连接间隔时长(分钟)
periodCheckConnectionTimeMin=1
#连接泄露检测
connectionLeakCheck=false
#连接泄露检测间隔时长(分钟)
connectionLeakCheckPeriodTimeMin=1
#强制归还连接时长(小时)
forceReturnConnectionTimeHour=0.01

2)测试代码

package me.lovegao.gdao.connpool;

import java.sql.Connection;
import java.util.Properties; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import me.lovegao.gdao.bean.SystemConstant;
import me.lovegao.gdao.connection.IConnectionPool;
import me.lovegao.gdao.connection.SimpleV2ConnectionPool; public class V2ConnectionPoolTest {
private final static Logger log = LoggerFactory.getLogger(V2ConnectionPoolTest.class); public static void main(String[] args) throws Exception {
String dbPath = "mysql2.properties";
log.info("hello-----------------");
log.warn("hello-----------------");
periodCheckConnection(dbPath);
} /**
* 连接泄露检测
* @param dbPath
* @throws Exception
*/
public static void checkConnectionLeak(String dbPath) throws Exception {
Properties dbProp = CommonUtil.loadProp(dbPath);
dbProp.setProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN, "10");
dbProp.setProperty(SystemConstant.STR_CONNECTION_LEAK_CHECK, "true");
IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp);
Connection conn = connPool.getConnection();
Thread.sleep(240000);
connPool.returnConnection(conn);
Thread.sleep(61000);
connPool.closeConnectionPool();
} /**
* 连接定时检测
* @param dbPath
* @throws Exception
*/
public static void periodCheckConnection(String dbPath) throws Exception {
Properties dbProp = CommonUtil.loadProp(dbPath);
dbProp.setProperty(SystemConstant.STR_PERIOD_CHECK_CONNECTION_TIME_MIN, "1");
IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp);
Thread.sleep(120000);
Connection conn = connPool.getConnection();
connPool.returnConnection(conn);
Thread.sleep(2061000);
// connPool.closeConnectionPool();
} /**
* 归还连接检测
* @param dbPath
* @throws Exception
*/
public static void checkWhenReturn(String dbPath) throws Exception {
Properties dbProp = CommonUtil.loadProp(dbPath);
IConnectionPool connPool = new SimpleV2ConnectionPool(dbProp);
Connection conn = connPool.getConnection();
conn.close();
connPool.returnConnection(conn);
Thread.sleep(20000);
connPool.closeConnectionPool();
} }

我是分布执行的测试,通过debug来校验的流程,面对这种项目,不知道单测该如何写。

我是通过中间把数据库关了,又打开,来连接定时检测的。结果证明没有问题。

框架优化版本已提交到git:https://github.com/shuimutong/gdao.git,欢迎指点

相关测试代码:https://github.com/shuimutong/useDemo.git  ./gdao-demo下。

-----------------------本文完------------------------------

手写DAO框架(七)-如何保证连接可用的更多相关文章

  1. 手写DAO框架(三)-数据库连接

    -------前篇:手写DAO框架(二)-开发前的最后准备--------- 前言 上一篇主要是温习了一下基础知识,然后将整个项目按照模块进行了划分.因为是个人项目,一个人开发,本人采用了自底向上的开 ...

  2. 手写DAO框架(二)-开发前的最后准备

    -------前篇:手写DAO框架(一)-从“1”开始 --------- 前言:前篇主要介绍了写此框架的动机,把主要功能点大致介绍了一下.此篇文章主要介绍开发前最后的一些准备.主要包括一些基础知识点 ...

  3. 手写DAO框架(一)-从“1”开始

    背景: 很久(4年)之前写了一个DAO框架-zxdata(https://github.com/shuimutong/zxdata),这是我写的第一个框架.因为没有使用文档,我现在如果要用的话,得从头 ...

  4. 手写DAO框架(四)-SQL执行

    -------前篇:手写DAO框架(三)-数据库连接--------- 前言 通过上一篇写的方法,可以灵活的获取.释放数据库连接,拿到连接之后,我们就可以执行sql了!所以,本篇介绍的就是SQL执行器 ...

  5. 手写DAO框架(五)-DAO层实现

    -------前篇:手写DAO框架(四)-SQL执行--------- 前言 通过上一篇,可以通过传入sql和对应的参数,可以执行sql并返回结果.但是对于一个DAO框架来说,要尽量的面向对象编程,也 ...

  6. 手写DAO框架(六)-框架使用示例

    一.引入pom <dependency> <groupId>me.lovegao</groupId> <artifactId>gdao</arti ...

  7. 手写MQ框架(一)-准备启程

    一.背景 很久以前写了DAO框架和MVC框架,前段时间又重写了DAO框架-GDAO(手写DAO框架(一)-从“1”开始,源码:https://github.com/shuimutong/gdao.gi ...

  8. 手写MQ框架(二)-服务端实现

    一.起航 书接上文->手写MQ框架(一)-准备启程 本着从无到有,从有到优的原则,所以计划先通过web实现功能,然后再优化改写为socket的形式. 1.关于技术选型 web框架使用了之前写的g ...

  9. 手写MVC框架(一)-再出发

    背景 前段时间把之前写的DAO框架(手写DAO框架(一)-从“1”开始)整理了一下,重构了一版.整理过程中看以前写的代码,只是为了了解实现,只是为了实现,代码写的有点粗糙.既然已经整理了DAO框架,索 ...

随机推荐

  1. 原创:【ajax | axios跨域简单请求+复杂请求】自定义header头Token请求Laravel5后台【亲测可用】

    如标题:我想在ajax的header头增加自定义Token进行跨域api认证并调用,api使用laravel5编写,如何实现? 首先,了解下CORS简单请求和复杂请求.  -- CORS简单请求 -- ...

  2. Filebeat的使用

    前言 logstash本身就可以具有文件数据采集的功能了,为什么还需要在前面加一层filebeat?理由如下:logstash是使用Java编写,插件是使用JRuby编写,对机器的资源要求会比较高,在 ...

  3. Laravel 控制器 Controller

    一.控制器存在的意义 路由可以分发请求:路由中还可以引入 html 页面:我们可以在 route/web.php 中搞定一切了:但是如果把业务逻辑都写入到路由中:那路由将庞大的难以维护:于是控制器就有 ...

  4. 什么时候会进行 SpringMVC重定向保存参数(FlashMap)?

    SpringMVC重定向保存参数(FlashMap):两种情况会保存参数:    1. 当前视图为RedirectView,也即是说当前请求为重定向请求.        org.springframe ...

  5. (转)react 项目构建

    原文:https://segmentfault.com/a/1190000016342792 写在前面 每次构建react项目的时候都会配置一大堆东西,时间久了就会忘记怎么配置.为了方便自己记忆也为了 ...

  6. Performance-Schema

    https://yq.aliyun.com/articles/640181?spm=a2c4e.11153940.0.0.347359d3DAu7sW MySQL Performance-Schema ...

  7. visual studio制作代码片段

    使用 Visual Studio 的代码片段功能,我们可以快速根据已有模板创建出大量常用的代码出来.ReSharper 已经自带了一份非常好用的代码片段工具,不过使用 ReSharper 创建出来的代 ...

  8. LeetCode 108. Convert Sorted Array to Binary Search Tree (将有序数组转换成BST)

    108. Convert Sorted Array to Binary Search Tree Given an array where elements are sorted in ascendin ...

  9. springboot 读取配置文件

    读取配置文件 在以前的项目中我们主要在 XML 文件中进行框架配置,业务的相关配置会放在属性文件中,然后通过一个属性读取的工具类来读取配置信息. 在 Spring Boot 中我们不再需要使用这种方式 ...

  10. Hadoop深入学习之HA

    1. 基本原理 2.x版本中,HDFS架构解决了单点故障问题,即引入双NameNode架构,同时借助共享存储系统来进行元数据的同步,共享存储系统类型一般有几类,如:Shared NAS+NFS.Boo ...