手写DAO框架(七)-如何保证连接可用
版权声明:本文为博客园博主「水木桶」的原创文章,遵循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框架(七)-如何保证连接可用的更多相关文章
- 手写DAO框架(三)-数据库连接
-------前篇:手写DAO框架(二)-开发前的最后准备--------- 前言 上一篇主要是温习了一下基础知识,然后将整个项目按照模块进行了划分.因为是个人项目,一个人开发,本人采用了自底向上的开 ...
- 手写DAO框架(二)-开发前的最后准备
-------前篇:手写DAO框架(一)-从“1”开始 --------- 前言:前篇主要介绍了写此框架的动机,把主要功能点大致介绍了一下.此篇文章主要介绍开发前最后的一些准备.主要包括一些基础知识点 ...
- 手写DAO框架(一)-从“1”开始
背景: 很久(4年)之前写了一个DAO框架-zxdata(https://github.com/shuimutong/zxdata),这是我写的第一个框架.因为没有使用文档,我现在如果要用的话,得从头 ...
- 手写DAO框架(四)-SQL执行
-------前篇:手写DAO框架(三)-数据库连接--------- 前言 通过上一篇写的方法,可以灵活的获取.释放数据库连接,拿到连接之后,我们就可以执行sql了!所以,本篇介绍的就是SQL执行器 ...
- 手写DAO框架(五)-DAO层实现
-------前篇:手写DAO框架(四)-SQL执行--------- 前言 通过上一篇,可以通过传入sql和对应的参数,可以执行sql并返回结果.但是对于一个DAO框架来说,要尽量的面向对象编程,也 ...
- 手写DAO框架(六)-框架使用示例
一.引入pom <dependency> <groupId>me.lovegao</groupId> <artifactId>gdao</arti ...
- 手写MQ框架(一)-准备启程
一.背景 很久以前写了DAO框架和MVC框架,前段时间又重写了DAO框架-GDAO(手写DAO框架(一)-从“1”开始,源码:https://github.com/shuimutong/gdao.gi ...
- 手写MQ框架(二)-服务端实现
一.起航 书接上文->手写MQ框架(一)-准备启程 本着从无到有,从有到优的原则,所以计划先通过web实现功能,然后再优化改写为socket的形式. 1.关于技术选型 web框架使用了之前写的g ...
- 手写MVC框架(一)-再出发
背景 前段时间把之前写的DAO框架(手写DAO框架(一)-从“1”开始)整理了一下,重构了一版.整理过程中看以前写的代码,只是为了了解实现,只是为了实现,代码写的有点粗糙.既然已经整理了DAO框架,索 ...
随机推荐
- 原创:【ajax | axios跨域简单请求+复杂请求】自定义header头Token请求Laravel5后台【亲测可用】
如标题:我想在ajax的header头增加自定义Token进行跨域api认证并调用,api使用laravel5编写,如何实现? 首先,了解下CORS简单请求和复杂请求. -- CORS简单请求 -- ...
- Filebeat的使用
前言 logstash本身就可以具有文件数据采集的功能了,为什么还需要在前面加一层filebeat?理由如下:logstash是使用Java编写,插件是使用JRuby编写,对机器的资源要求会比较高,在 ...
- Laravel 控制器 Controller
一.控制器存在的意义 路由可以分发请求:路由中还可以引入 html 页面:我们可以在 route/web.php 中搞定一切了:但是如果把业务逻辑都写入到路由中:那路由将庞大的难以维护:于是控制器就有 ...
- 什么时候会进行 SpringMVC重定向保存参数(FlashMap)?
SpringMVC重定向保存参数(FlashMap):两种情况会保存参数: 1. 当前视图为RedirectView,也即是说当前请求为重定向请求. org.springframe ...
- (转)react 项目构建
原文:https://segmentfault.com/a/1190000016342792 写在前面 每次构建react项目的时候都会配置一大堆东西,时间久了就会忘记怎么配置.为了方便自己记忆也为了 ...
- Performance-Schema
https://yq.aliyun.com/articles/640181?spm=a2c4e.11153940.0.0.347359d3DAu7sW MySQL Performance-Schema ...
- visual studio制作代码片段
使用 Visual Studio 的代码片段功能,我们可以快速根据已有模板创建出大量常用的代码出来.ReSharper 已经自带了一份非常好用的代码片段工具,不过使用 ReSharper 创建出来的代 ...
- 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 ...
- springboot 读取配置文件
读取配置文件 在以前的项目中我们主要在 XML 文件中进行框架配置,业务的相关配置会放在属性文件中,然后通过一个属性读取的工具类来读取配置信息. 在 Spring Boot 中我们不再需要使用这种方式 ...
- Hadoop深入学习之HA
1. 基本原理 2.x版本中,HDFS架构解决了单点故障问题,即引入双NameNode架构,同时借助共享存储系统来进行元数据的同步,共享存储系统类型一般有几类,如:Shared NAS+NFS.Boo ...