转载:https://blog.csdn.net/neven7/article/details/50782451

0.背景

性能测试工具nGrinder支持在无需修改源码的情况下,对目标服务器收集自定义数据,最多支持5类;

在性能测试详细报告页,目标服务器->你的机器ip便签页下,默认只收集CPU, Memory, Received Byte/s, Sent Byte Per Secode/s等4类数据;

可能你还需要监控其它的性能统计数据,用于分析(比如load, Full Gc);本文先介绍实现方法;再分析nGrinder源码,看它是怎么实现的。

1.实现

1-1. 安装monitor

在你的nGrinder系统下,下载监控

安装到你测试服务所在的机器,解压tar包,执行sh run_monitor_bg.sh;

其实脚本是启了个java服务,以monitor模式启动;

之前介绍过Agent有2种模式:

gent mode: 运行进程和线程,压测目标服务;

monitor mode: 监控目标系统性能(cpu/memory)。

[root@10 ngrinder-monitor]# cat run_monitor.sh
#!/bin/sh
curpath=`dirname $0`
cd ${curpath}
java -server -cp "lib/*" org.ngrinder.NGrinderAgentStarter --mode monitor --command run $@
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Agent的home路径为/root/.ngrinder_agent,你在执行sh run_monitor_bg.sh默认获取的配置信息为/root/.ngrinder_agent/agent.conf; 如果加上-o,sh run_monitor_bg.sh 读取你安装monitor目录下的__agent.conf, 该配置文件定义了Agent的模式,ip, 端口。

[root@10 .ngrinder_agent]# cat agent.conf
common.start_mode=monitor
#If you want to monitor bind to the different local ip not automatically selected ip. Specify below field.
#monitor.binding_port=hostname_or_ip
monitor.binding_port=13243
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

自定义数据需放在/root/.ngrinder_agent/monitor/custom.data文件里,格式如下:

类型1数据,类型2数据,类型3数据,类型4数据,类型5数据
  • 1

最多支持5类,每类数据用“,”分隔,注意的是: 数据是实时的写文件,不是累积数据到文件中(类似shell中的>, 不是>>),即同一时刻,只有一行数据。

1-2. 定制收集脚本

以收集load和full GC为例:

[root@10 bin]# cat updateCustomData.sh
#!/bin/sh
#@author hugang customDataRoot=/root/.ngrinder_agent/monitor/custom.data;
# 获取load信息
load=`/bin/cat /proc/loadavg | awk '{print $1}'`;
# 获取full gc count
if [[ $1 -gt 0 ]]; then
fgc=`jstat -gcutil $1 | tail -1 | awk '{print $8}'`;
echo $load,$fgc > $customDataRoot;
else
echo $load > $customDataRoot;
fi;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

开始性能测试时,每秒去执行该脚本,收集数据到custom.data中:

 watch -n 1 sh updateCustomData.sh 5528
  • 1

5528为需监控java服务进程pid;

当你性能测试结束后,monitor收集的数据会放到/root/.ngrinder/perftest/0_999/{test_id}/report/monitor_system_{test_id}/report/monitor_system_{ip}.data文件中:

[root@10 report]# cat monitor_system_10.13.1.139.data
ip,system,collectTime,freeMemory,totalMemory,cpuUsedPercentage,receivedPerSec,sentPerSec,customValues
10.13.1.139,LINUX,20160302151441,97102768,132112072,26.895683,32954,27897,4.93,49
10.13.1.139,LINUX,20160302151443,97075896,132112072,30.513468,45702,32306,4.93,49
10.13.1.139,LINUX,20160302151445,97034772,132112072,30.411074,110306,65391,5.02,49
10.13.1.139,LINUX,20160302151447,96972504,132112072,22.073017,84813,57503,5.02,49
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

1-3.结果展示

2.源码分析

nGrinder使用Sigar工具(https://support.hyperic.com/display/SIGAR/Home)收集系统信息,该工具可以收集以下数据:

System memory, swap, cpu, load average, uptime, logins
Per-process memory, cpu, credential info, state, arguments, environment, open files
File system detection and metrics
Network interface detection, configuration info and metrics
TCP and UDP connection tables
Network route table
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

sigar工具(http://download.csdn.net/download/neven7/9450930)示例:

[root@10 testsigar]# ls
libsigar-amd64-linux.so sigar-1.6.4.jar sigar-1.6.4.jar.zip
[root@10 testsigar]#
[root@10 testsigar]# java -jar ./sigar-1.6.4.jar
sigar> free
total used free
Mem: 132112072 96855372 35256700
-/+ buffers/cache: 34855500 97256572
Swap: 8388600 264980 8123620
RAM: 129016MB
sigar>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

收集系统数据的java文件为: 
ngrinder-core/src/main/java/org/ngrinder/monitor/collector/SystemDataCollector.java

继承和实现关系:

SystemDataCollector extends DataCollector 

DataCollector implements Runnable
  • 1
  • 2
  • 3
  • 4

SystemDataCollector的线程执行体:

public void run() {
// 初始化sigar
initSigar();
SystemMonitoringData systemMonitoringData = (SystemMonitoringData) getMXBean(SYSTEM);
// execute()通过sigar api获取系统信息
systemMonitoringData.setSystemInfo(execute());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

execute()获取系统信息SystemInfo(System info object to save date collected by monitor):

/**
* Execute the collector to get the system info model.
*
* @return SystemInfo in current time
*/
public synchronized SystemInfo execute() {
SystemInfo systemInfo = new SystemInfo();
systemInfo.setCollectTime(System.currentTimeMillis());
try {
BandWidth networkUsage = getNetworkUsage();
BandWidth bandWidth = networkUsage.adjust(prev.getBandWidth());
systemInfo.setBandWidth(bandWidth);
systemInfo.setCPUUsedPercentage((float) sigar.getCpuPerc().getCombined() * 100);
Cpu cpu = sigar.getCpu();
systemInfo.setTotalCpuValue(cpu.getTotal());
systemInfo.setIdleCpuValue(cpu.getIdle());
Mem mem = sigar.getMem();
systemInfo.setTotalMemory(mem.getTotal() / 1024L);
systemInfo.setFreeMemory(mem.getActualFree() / 1024L);
systemInfo.setSystem(OperatingSystem.IS_WIN32 ? SystemInfo.System.WINDOW : SystemInfo.System.LINUX);
systemInfo.setCustomValues(getCustomMonitorData());
} catch (Throwable e) {
LOGGER.error("Error while getting system perf data:{}", e.getMessage());
LOGGER.debug("Error trace is ", e);
}
prev = systemInfo;
return systemInfo;
}
  • 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

其中:getCustomMonitorData()获取自定义数据,读取custom.data文件中一行数据

private String getCustomMonitorData() {
if (customDataFile != null && customDataFile.exists()) {
BufferedReader customDataFileReader = null;
try {
customDataFileReader = new BufferedReader(new FileReader(customDataFile));
return customDataFileReader.readLine(); // these data will be parsed at
// monitor client side.
} catch (IOException e) {
// Error here is very natural
LOGGER.debug("Error to read custom monitor data", e);
} finally {
IOUtils.closeQuietly(customDataFileReader);
}
}
return prev.getCustomValues();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

综上:类SystemDataCollector作用就是作为线程执行体,线程每次执行通过sigar获取系统信息:SystemInfo,赋值给SystemMonitoringData成员变量SystemInfo。

前面介绍启动monitor时,其实是执行了org.ngrinder.NGrinderAgentStarter类,我们再分析下该文件,ngrinder-core/src/main/java/org/ngrinder/NGrinderAgentStarter.java

/**
* Agent starter.
*
* @param args arguments
*/
public static void main(String[] args) {
NGrinderAgentStarter starter = new NGrinderAgentStarter();
final NGrinderAgentStarterParam param = new NGrinderAgentStarterParam();
checkJavaVersion();
JCommander commander = new JCommander(param);
commander.setProgramName("ngrinder-agent");
commander.setAcceptUnknownOptions(true);
try {
commander.parse(args);
} catch (Exception e) {
LOG.error(e.getMessage());
return;
}
final List<String> unknownOptions = commander.getUnknownOptions();
modeParam = param.getModeParam();
modeParam.parse(unknownOptions.toArray(new String[unknownOptions.size()])); if (modeParam.version != null) {
System.out.println("nGrinder v" + getStaticVersion());
return;
} if (modeParam.help != null) {
modeParam.usage();
return;
} System.getProperties().putAll(modeParam.params);
starter.init(); final String startMode = modeParam.name();
if ("stop".equalsIgnoreCase(param.command)) {
starter.stopProcess(startMode);
System.out.println("Stop the " + startMode);
return;
}
starter.checkDuplicatedRun(startMode);
if (startMode.equalsIgnoreCase("agent")) {
starter.startAgent();
} else if (startMode.equalsIgnoreCase("monitor")) {
starter.startMonitor();
} else {
staticPrintHelpAndExit("Invalid agent.conf, '--mode' must be set as 'monitor' or 'agent'.");
}
}
  • 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

monitor模式执行该方法:starter.startMonitor()


/**
* Start the performance monitor.
*/
public void startMonitor() {
printLog("***************************************************");
printLog("* Start nGrinder Monitor... ");
printLog("***************************************************");
try {
MonitorServer.getInstance().init(agentConfig);
MonitorServer.getInstance().start();
} catch (Exception e) {
LOG.error("ERROR: {}", e.getMessage());
printHelpAndExit("Error while starting Monitor", e);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

MonitorServer.getInstance().start():

    /**
* Start monitoring.
*
* @throws IOException exception
*/
public void start() throws IOException {
if (!isRunning()) {
jmxServer.start();
DataCollectManager.getInstance().init(agentConfig);
DataCollectManager.getInstance().start();
isRunning = true;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

DataCollectManager.getInstance().start();

    /**
* start a scheduler for the data collector jobs.
*/
public void start() {
int collectorCount = MXBeanStorage.getInstance().getSize();
scheduler = Executors.newScheduledThreadPool(collectorCount);
if (!isRunning()) {
Collection<MXBean> mxBeans = MXBeanStorage.getInstance().getMXBeans();
for (MXBean mxBean : mxBeans) {
DataCollector collector = mxBean.gainDataCollector(agentConfig.getHome().getDirectory());
scheduler.scheduleWithFixedDelay(collector, 0L, getInterval(), TimeUnit.SECONDS);
LOG.info("{} started.", collector.getClass().getSimpleName());
}
LOG.info("Collection interval : {}s).", getInterval());
isRunning = true;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

scheduler.scheduleWithFixedDelay(collector, 0L, getInterval(), TimeUnit.SECONDS);

线程池周期地执行SystemDataCollector中run()去获取系统数据。

    @Override
public void run() {
initSigar();
SystemMonitoringData systemMonitoringData = (SystemMonitoringData) getMXBean(SYSTEM);
systemMonitoringData.setSystemInfo(execute());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.总结:

后台启动的monitor, 运行的是一个java服务:

java -server -cp lib/* org.ngrinder.NGrinderAgentStarter --mode monitor --command run
  • 1

通过线程池周期获取系统性信息(sigar工具获取),存放在SystemInfo;

ngrinder-controller/src/main/java/org/ngrinder/perftest/service/samplinglistener/MonitorCollectorPlugin.java中startSampling():

@Override
public void startSampling(final ISingleConsole singleConsole, PerfTest perfTest,
IPerfTestService perfTestService) {
final List<String> targetHostIP = perfTest.getTargetHostIP();
final Integer samplingInterval = perfTest.getSamplingInterval();
for (final String target : targetHostIP) {
scheduledTaskService.runAsync(new Runnable() {
@Override
public void run() {
LOGGER.info("Start JVM monitoring for IP:{}", target);
MonitorClientService client = new MonitorClientService(target, MonitorCollectorPlugin.this.port);
client.init();
if (client.isConnected()) {
File testReportDir = singleConsole.getReportPath();
File dataFile = null;
try {
dataFile = new File(testReportDir, MONITOR_FILE_PREFIX + target + ".data");
FileWriter fileWriter = new FileWriter(dataFile, false);
BufferedWriter bw = new BufferedWriter(fileWriter);
// write header info
bw.write(SystemInfo.HEADER);
bw.newLine();
bw.flush();
clientMap.put(client, bw);
} catch (IOException e) {
LOGGER.error("Error to write to file:{}, Error:{}", dataFile.getPath(), e.getMessage());
}
}
}
});
}
assignScheduledTask(samplingInterval);
}
  • 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

根据SystemInfo写到/root/.ngrinder/perftest/0_999/{test_id}/report/monitor_system_{test_id}/report/monitor_system_{ip}.data文件中;

ngrinder-controller/src/main/java/org/ngrinder/perftest/PerfTestService.java中getMonitorGraph()根据/root/.ngrinder/perftest/0_999/{test_id}/report/monitor_system_{test_id}/report/monitor_system_{ip}.data获取系统信息数据

     /**
* Get system monitor data and wrap the data as a string value like "[22,11,12,34,....]", which can be used directly
* in JS as a vector.
*
* @param testId test id
* @param targetIP ip address of the monitor target
* @param dataInterval interval value to get data. Interval value "2" means, get one record for every "2" records.
* @return return the data in map
*/
public Map<String, String> getMonitorGraph(long testId, String targetIP, int dataInterval) {
Map<String, String> returnMap = Maps.newHashMap();
File monitorDataFile = new File(config.getHome().getPerfTestReportDirectory(String.valueOf(testId)),
MONITOR_FILE_PREFIX + targetIP + ".data");
BufferedReader br = null;
try { StringBuilder sbUsedMem = new StringBuilder("[");
StringBuilder sbCPUUsed = new StringBuilder("[");
StringBuilder sbNetReceived = new StringBuilder("[");
StringBuilder sbNetSent = new StringBuilder("[");
StringBuilder customData1 = new StringBuilder("[");
StringBuilder customData2 = new StringBuilder("[");
StringBuilder customData3 = new StringBuilder("[");
StringBuilder customData4 = new StringBuilder("[");
StringBuilder customData5 = new StringBuilder("["); br = new BufferedReader(new FileReader(monitorDataFile));
br.readLine(); // skip the header.
// "ip,system,collectTime,freeMemory,totalMemory,cpuUsedPercentage,receivedPerSec,sentPerSec"
String line = br.readLine();
int skipCount = dataInterval;
// to be compatible with previous version, check the length before
// adding
while (StringUtils.isNotBlank(line)) {
if (skipCount < dataInterval) {
skipCount++;
} else {
skipCount = 1;
String[] datalist = StringUtils.split(line, ",");
if ("null".equals(datalist[4]) || "undefined".equals(datalist[4])) {
sbUsedMem.append("null").append(",");
} else {
sbUsedMem.append(Long.valueOf(datalist[4]) - Long.valueOf(datalist[3])).append(",");
}
addCustomData(sbCPUUsed, 5, datalist);
addCustomData(sbNetReceived, 6, datalist);
addCustomData(sbNetSent, 7, datalist);
addCustomData(customData1, 8, datalist);
addCustomData(customData2, 9, datalist);
addCustomData(customData3, 10, datalist);
addCustomData(customData4, 11, datalist);
addCustomData(customData5, 12, datalist);
line = br.readLine();
}
}
completeCustomData(returnMap, "cpu", sbCPUUsed);
completeCustomData(returnMap, "memory", sbUsedMem);
completeCustomData(returnMap, "received", sbNetReceived);
completeCustomData(returnMap, "sent", sbNetSent);
completeCustomData(returnMap, "customData1", customData1);
completeCustomData(returnMap, "customData2", customData2);
completeCustomData(returnMap, "customData3", customData3);
completeCustomData(returnMap, "customData4", customData4);
completeCustomData(returnMap, "customData5", customData5);
} catch (IOException e) {
LOGGER.info("Error while getting monitor {} data file at {}", targetIP, monitorDataFile);
} finally {
IOUtils.closeQuietly(br);
}
return returnMap;
}
  • 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

数据提供给Controller端: 
ngrinder-controller/src/man/java/org/ngrinder/perftest/controller/PerfTestController.java

    private Map<String, String> getMonitorGraphData(long id, String targetIP, int imgWidth) {
int interval = perfTestService.getMonitorGraphInterval(id, targetIP, imgWidth);
Map<String, String> sysMonitorMap = perfTestService.getMonitorGraph(id, targetIP, interval);
PerfTest perfTest = perfTestService.getOne(id);
sysMonitorMap.put("interval", String.valueOf(interval * (perfTest != null ? perfTest.getSamplingInterval() : 1)));
return sysMonitorMap;
} /**
* Get the monitor data of the target having the given IP.
*
* @param id test Id
* @param targetIP targetIP
* @param imgWidth image width
* @return json message
*/
@RestAPI
@RequestMapping("/api/{id}/monitor")
public HttpEntity<String> getMonitorGraph(@PathVariable("id") long id,
@RequestParam("targetIP") String targetIP, @RequestParam int imgWidth) {
return toJsonHttpEntity(getMonitorGraphData(id, targetIP, imgWidth));
}

nGrinder对监控机器收集自定义数据及源码分析的更多相关文章

  1. HDFS源码分析数据块复制监控线程ReplicationMonitor(二)

    HDFS源码分析数据块复制监控线程ReplicationMonitor(二)

  2. HDFS源码分析数据块复制监控线程ReplicationMonitor(一)

    ReplicationMonitor是HDFS中关于数据块复制的监控线程,它的主要作用就是计算DataNode工作,并将复制请求超时的块重新加入到待调度队列.其定义及作为线程核心的run()方法如下: ...

  3. JUnit源码分析 - 扩展 - 自定义Rule

    JUnit Rule简述 Rule是JUnit 4.7之后新加入的特性,有点类似于拦截器,可以在测试类或测试方法执行前后添加额外的处理,本质上是对@BeforeClass, @AfterClass, ...

  4. JUnit源码分析 - 扩展 - 自定义RunListener

    RunListener简述 JUnit4中的RunListener类用来监听测试执行的各个阶段,由RunNotifier通知测试去运行.RunListener与RunNotifier之间的协作应用的是 ...

  5. 简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析

    简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析 虽然经常用 OAuth 2.0,但是原理却不曾了解,印象里觉得很简单,请求跳来跳去,今天看完相关介绍,就来捋一捋 ...

  6. ArrayList 源码分析和自定义ArrayList实现

    概述 ArrayList 是基于数组实现的,是一个能自动扩展的动态数组. ArrayList 是线程不安全的,多线程情况下添加元素会出现数组越界的情况,而且数组赋值操作不是原子操作,会导致多线程情况下 ...

  7. jQuery1.9.1源码分析--数据缓存Data模块

    jQuery1.9.1源码分析--数据缓存Data模块 阅读目录 jQuery API中Data的基本使用方法介绍 jQuery.acceptData(elem)源码分析 jQuery.data(el ...

  8. druid 源码分析与学习(含详细监控设计思路的彩蛋)(转)

    原文路径:http://herman-liu76.iteye.com/blog/2308563  Druid是阿里巴巴公司的数据库连接池工具,昨天突然想学习一下阿里的druid源码,于是下载下来分析了 ...

  9. asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证

    原文:asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证 在前面的文章中我们曾经涉及到ControllerActionInvoker类GetPara ...

随机推荐

  1. CF 277.5 A.SwapSort 水题

    //STL教你做人系列 #include<stdio.h> #include<iostream> #include<math.h> #include<algo ...

  2. C#高级编程9-第6章 数组

    数组 1.同一类型和不同类型的多个对象 需要使用同一类型的多个对象则使用集合和数组. Array类为数组元素排序和过滤提供了多个方法,使用枚举器,可以迭代数组所有元素. 2.简单数组 数组的声明 因为 ...

  3. Linux性能监控分析命令(三)—iostat命令介绍

    性能监控分析的命令包括如下: 1.vmstat 2.sar 3.iostat 4.top 5.free 6.uptime 7.netstat 8.ps 9.strace 10.lsof 命令介绍: i ...

  4. python知识(2)----python的多态

    python不支持多态,但他是一种多态语言,看下面这篇博客: 参考资料: [1]. 再谈python中的多态

  5. 理解linux下源码、yum和rpm安装方法的特点

    1.yum可看作在线安装,只需yum install 软件名,系统就自动根据yum源配置文件中的镜像位置去下载安装包,并可以自动分析所需的软件依赖关系,自动安装所需的依赖软件包.简单方便,不易出错,不 ...

  6. IIS Express并发数设置

    今天将之前的一个瓦片图的服务迁移到了asp.net core试了一下,使用的时候感觉客户端刷新时有些慢,估计是并发连接数限制的原因. 由于这是一个开发中的版本,是用IIS Express部署的,IIS ...

  7. Tasker to answer incoming call by pressing power button

    nowadays, the smartphone is getting bigger in size, eg. samsung galaxy note and note 2, sorta big in ...

  8. SSH深度历险(四) Maven初步学�

    这几天接触这个词,非常多遍了,仅仅是浅显的体会到它在GXPT中的优点,功能之强大,又通过网络查询了资料进一步的认识学习了,和大家分享. Maven是基于项目对象模型(POM),能够通过一小段描写叙述信 ...

  9. MySQL数据库事务各隔离级别加锁情况--Repeatable Read && MVCC(转)

    本文转自https://m.imooc.com/article/details?article_id=17289 感谢作者 上节回顾 上两篇记录了我对MySQL 事务 隔离级别read uncommi ...

  10. Easing圆环动画

    Easing圆环动画 效果 源码 https://github.com/YouXianMing/Animations // // CircleView.h // YXMWeather // // Cr ...