hbase源码系列(十二)Get、Scan在服务端是如何处理?
继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以走这个过程,所以就一起写了。
Get
我们打开HRegionServer找到get方法。Get的方法处理分两种,设置了ClosestRowBefore和没有设置的,一般来讲,我们都是知道了明确的rowkey,不太会设置这个参数,它默认是false的。
if (get.hasClosestRowBefore() && get.getClosestRowBefore()) {
byte[] row = get.getRow().toByteArray();
byte[] family = get.getColumn(0).getFamily().toByteArray();
r = region.getClosestRowBefore(row, family);
} else {
Get clientGet = ProtobufUtil.toGet(get);
if (existence == null) {
r = region.get(clientGet);
}
}
所以我们走的是HRegion的get方法,杀过去。
public Result get(final Get get) throws IOException {
checkRow(get.getRow(), "Get");
// 检查列族,以下省略代码一百字
List<Cell> results = get(get, true);
return Result.create(results, get.isCheckExistenceOnly() ? !results.isEmpty() : null);
}
先检查get的row是否在这个region里面,然后检查列族,如果没有的话,它会根据表定义给补全的,然后它转身又进入了另外一个get方法,真是狠心啊!
List<Cell> results = new ArrayList<Cell>();
Scan scan = new Scan(get);
RegionScanner scanner = null;
try {
scanner = getScanner(scan);
scanner.next(results);
} finally {
if (scanner != null)
scanner.close();
}
从上面可以看得出来,为什么我要把get和Scanner一起讲了吧,因为get也是一种特殊的Scan的方法,它只寻找一个row的数据。
Scan
下面开始讲Scan,在《HTable探秘》里面有个细节不知道注意到没,在查询之前,它要先OpenScanner获得要给ScannerId,这个OpenScanner其实也调用了scan方法,但是它过去不是干活的,而是先过去注册一个Scanner,订个租约,然后再把这个返回的ScannerId再次发送一个scan请求,这次才开始调用开始扫描。
扫描的时候,走的是这一段
if (!done) {
long maxResultSize = scanner.getMaxResultSize();
if (maxResultSize <= 0) {
maxResultSize = maxScannerResultSize;
}
List<Cell> values = new ArrayList<Cell>();
MultiVersionConsistencyControl.setThreadReadPoint(scanner.getMvccReadPoint());
region.startRegionOperation(Operation.SCAN);
try {
int i = 0;
synchronized(scanner) {
for (; i < rows && currentScanResultSize < maxResultSize; i++) {
// 它用的是这个nextRaw方法
boolean moreRows = scanner.nextRaw(values);
if (!values.isEmpty()) {
results.add(Result.create(values));
}
if (!moreRows) {
break;
}
values.clear();
}
}
} finally {
region.closeRegionOperation();
}
}
// 没找到设置moreResults为false,找到了把结果添加到builder里面去
if (scanner.isFilterDone() && results.isEmpty()) {
moreResults = false;
results = null;
} else {
addResults(builder, results, controller);
}
}
}
这里面有controller和result,这块的话,我求证了一下RpcServer那块,如果Rpc传输的时候使用了codec来压缩的话,就用controller返回结果,否则用response返回。
这块就不管了不是重点,下面我们看一下RegionScanner。
RegionScanner详解与代码拆分
我们冲过去看RegionScannerImpl吧,它在HRegion里面,我们直接去看nextRaw方法就可以了,get方法的那个next方法也是调用了nextRaw方法。
if (outResults.isEmpty()) { // 把结果存到outResults当中
returnResult = nextInternal(outResults, limit);
} else {
List<Cell> tmpList = new ArrayList<Cell>();
returnResult = nextInternal(tmpList, limit);
outResults.addAll(tmpList);
}
去nextInternal方法吧,这方法真大,尼玛,我要歇菜了,我们进入下一个阶段吧。
/** 把查询出来的结果保存到results当中 */
private boolean nextInternal(List<Cell> results, int limit)
throws IOException {
while (true) {
//从storeHeap里面取出一个来
KeyValue current = this.storeHeap.peek();
byte[] currentRow = null;
int offset = 0;
short length = 0;
if (current != null) {
currentRow = current.getBuffer();
offset = current.getRowOffset();
length = current.getRowLength();
}
//检查一下到这个row是否应该停止了
boolean stopRow = isStopRow(currentRow, offset, length);
if (joinedContinuationRow == null) {
// 如果要停止了,就用filter的filterRowCells过滤一下results.
if (stopRow) {
if (filter != null && filter.hasFilterRow()) {
//使用filter过滤掉一些cells
filter.filterRowCells(results);
}
return false;
}
// 如果有filter的话,过滤通过
if (filterRowKey(currentRow, offset, length)) {
boolean moreRows = nextRow(currentRow, offset, length);
if (!moreRows) return false;
results.clear();
continue;
}
//把结果保存到results当中
KeyValue nextKv = populateResult(results, this.storeHeap, limit, currentRow, offset,
length);
// Ok, we are good, let's try to get some results from the main heap.
// 在populateResult找到了足够limit数量的
if (nextKv == KV_LIMIT) {
if (this.filter != null && filter.hasFilterRow()) {
throw new IncompatibleFilterException(
"Filter whose hasFilterRow() returns true is incompatible with scan with limit!");
}
return true; // We hit the limit.
}
stopRow = nextKv == null ||
isStopRow(nextKv.getBuffer(), nextKv.getRowOffset(), nextKv.getRowLength());
// save that the row was empty before filters applied to it.
final boolean isEmptyRow = results.isEmpty();
// We have the part of the row necessary for filtering (all of it, usually).
// First filter with the filterRow(List). 过滤一下刚才找出来的
if (filter != null && filter.hasFilterRow()) {
filter.filterRowCells(results);
}
//如果result的空的,啥也没找到,这是。。。悲剧啊
if (isEmptyRow) {
boolean moreRows = nextRow(currentRow, offset, length);
if (!moreRows) return false;
results.clear();
// This row was totally filtered out, if this is NOT the last row,
// we should continue on. Otherwise, nothing else to do.
if (!stopRow) continue;
return false;
}
// Ok, we are done with storeHeap for this row.
// Now we may need to fetch additional, non-essential data into row.
// These values are not needed for filter to work, so we postpone their
// fetch to (possibly) reduce amount of data loads from disk.
if (this.joinedHeap != null) {
KeyValue nextJoinedKv = joinedHeap.peek();
// If joinedHeap is pointing to some other row, try to seek to a correct one.
boolean mayHaveData =
(nextJoinedKv != null && nextJoinedKv.matchingRow(currentRow, offset, length))
|| (this.joinedHeap.requestSeek(KeyValue.createFirstOnRow(currentRow, offset, length),
true, true)
&& joinedHeap.peek() != null
&& joinedHeap.peek().matchingRow(currentRow, offset, length));
if (mayHaveData) {
joinedContinuationRow = current;
populateFromJoinedHeap(results, limit);
}
}
} else {
// Populating from the joined heap was stopped by limits, populate some more.
populateFromJoinedHeap(results, limit);
}
// We may have just called populateFromJoinedMap and hit the limits. If that is
// the case, we need to call it again on the next next() invocation.
if (joinedContinuationRow != null) {
return true;
}
// Finally, we are done with both joinedHeap and storeHeap.
// Double check to prevent empty rows from appearing in result. It could be
// the case when SingleColumnValueExcludeFilter is used.
if (results.isEmpty()) {
boolean moreRows = nextRow(currentRow, offset, length);
if (!moreRows) return false;
if (!stopRow) continue;
}
// We are done. Return the result.
return !stopRow;
}
}
上面那段代码真的很长很臭,尼玛。。被我折叠起来了,有兴趣的看一眼就行,我们先分解开来看吧,这里面有两个Heap,一个是storeHeap,一个是JoinedHeap,他们啥时候用呢?看一下它的构造方法吧
for (Map.Entry<byte[], NavigableSet<byte[]>> entry :
scan.getFamilyMap().entrySet()) {
//遍历列族和列的映射关系,设置store相关的内容
Store store = stores.get(entry.getKey());
KeyValueScanner scanner = store.getScanner(scan, entry.getValue());
if (this.filter == null || !scan.doLoadColumnFamiliesOnDemand()
|| this.filter.isFamilyEssential(entry.getKey())) {
scanners.add(scanner);
} else {
joinedScanners.add(scanner);
}
}
this.storeHeap = new KeyValueHeap(scanners, comparator);
if (!joinedScanners.isEmpty()) {
this.joinedHeap = new KeyValueHeap(joinedScanners, comparator);
}
}
如果joinedScanners不空的话,就new一个joinedHeap出来,但是我们看看它的成立条件,有点儿难吧。
1、filter不为null
2、scan设置了doLoadColumnFamiliesOnDemand为true
3、设置了的filter的isFamilyEssential方法返回false,这个估计得自己写一个,因为我刚才去看了几个filter的这个方法默认都是用的FilterBase的方法返回false。
好的,到这里我们有可以把上面那段代码砍掉很大一部分了,它的成立条件比较困难,所以很难出现了,那我们就挑重点的storeHeap来讲吧,我们先看着这三行。
Store store = stores.get(entry.getKey()); KeyValueScanner scanner = store.getScanner(scan, entry.getValue()); this.storeHeap = new KeyValueHeap(scanners, comparator);
通过列族获得相应的Store,然后通过getScanner返回scanner加到KeyValueHeap当中,我们应该去刺探一下HStore的getScanner方法,它new了一个StoreScanner返回,继续看StoreScanner。
public StoreScanner(Store store, ScanInfo scanInfo, Scan scan, final NavigableSet<byte[]> columns) throws IOException {
matcher = new ScanQueryMatcher(scan, scanInfo, columns,
ScanType.USER_SCAN, Long.MAX_VALUE, HConstants.LATEST_TIMESTAMP,
oldestUnexpiredTS);
// 返回MemStore、所有StoreFile的Scanner.
List<KeyValueScanner> scanners = getScannersNoCompaction();
//explicitColumnQuery:是否过滤列族 lazySeekEnabledGlobally默认是true 如果文件数量超过1个,isParallelSeekEnabled就是true
if (explicitColumnQuery && lazySeekEnabledGlobally) {
for (KeyValueScanner scanner : scanners) {
scanner.requestSeek(matcher.getStartKey(), false, true);
}
} else {
if (!isParallelSeekEnabled) {
for (KeyValueScanner scanner : scanners) {
scanner.seek(matcher.getStartKey());
}
} else {
//一般走这里,并行查
parallelSeek(scanners, matcher.getStartKey());
}
}
// 一个堆里面包括了两个scanner,MemStore、StoreFile的Scanner
heap = new KeyValueHeap(scanners, store.getComparator());
this.store.addChangedReaderObserver(this);
}
对上面的代码,我们再慢慢来分解。
1、先new了一个ScanQueryMatcher,它是一个用来过滤的类,传参数的时候,需要传递scan和oldestUnexpiredTS进去,oldestUnexpiredTS是个参数,是(当前时间-列族的生存周期),小于这个时间戳的kv视为已经过期了,在它初始化的时候,我们注意一下它的startKey和stopRow,这个startKey要注意,它可不是我们设置的那个startRow,而是用这个startRow来new了一个DeleteFamily类型的KeyValue。
this.stopRow = scan.getStopRow(); this.startKey = KeyValue.createFirstDeleteFamilyOnRow(scan.getStartRow())
2、接着我们看getScannersNoCompaction这个方法,它这里是返回了两个Scanner,MemStoreScanner和所有StoreFile的Scanner,在从StoreHeap中peak出来一个kv的时候,是从他们当中交替取出kv来的,StoreHeap从它的名字上面来看像是用了堆排序的算法,它的peek方法和next方法真有点儿复杂,下一章讲MemStore的时候再讲吧。
//获取所有的storefile,默认的实现没有用上startRow和stopRow storeFilesToScan = this.storeEngine.getStoreFileManager().getFilesForScanOrGet(isGet, startRow, stopRow); memStoreScanners = this.memstore.getScanners();
默认的getStoreFileManager的getFilesForScanOrGet是返回了所有的StoreFile的Scanner,而不是通过startRow和stopRow做过滤,它的注释里面给出的解释,里面的files默认是按照seq id来排序的,而不是startKey,需要优化的可以从这里下手。
3、然后就开始先seek一下,而不是全表扫啊!
//过滤列族的情况 scanner.requestSeek(matcher.getStartKey(), false, true); //一般走这里,并行查 parallelSeek(scanners, matcher.getStartKey());
scanner.requestSeek不是所有情况都要seek,是查询Delete的时候,如果查询的kv的时间戳比文件的最大时间戳小,就seek到上次未查询到的kv;它这里可能会用上DeleteFamily删除真个family这种情况。
parallelSeek就是开多线程去调用Scanner的seek方法, MemStore的seek很简单,因为它的kv集合是一个排序好的集合,HFile的seek比较复杂,下面我用一个图来表达吧。

在搜索HFile的时候,key先从一级索引找,通过它定位到细的二级索引,然后再定位到具体的block上面,到了HFileBlock之后,就不是seek了,就是遍历,遍历没什么好说的,不熟悉的朋友建议先回去看看《StoreFile存储格式》。注意哦,这个key就是我们的startKey哦,所以大家知道为什么要在scan的时候要设置StartKey了吗?
nextInternal的流程
通过前面的分析,我们可以把nextInternal分解与拆分、抹去一些不必要的代码,我发现代码还是很难懂,所以我画了一个过程图出来代替那段代码。

特别注意事项:
1、这个图是被我处理过的简化之后的图,还有在放弃该row的kv们 之后并非都要进行是StopRow的判断,只是为了合并这个流程,我加上去的isStopRow的判断,但并不影响整个流程。
2、!isStopRow代表返回代码的(!isStopRow)的意思, 根据isStopRow的当前值来返回true或者false
3、true意味着退出,并且还有结果,false意味着退出,没有结果
诶,看到这里,还是没看到它是怎么用ScanQueryMatcher去过滤被删除的kv们啊,好,接下来我们重点考察这个问题。
ScanQueryMatcher如何过滤已经被删除的KeyValue
这个过程屏蔽在了filterRow之后通过的把该row的kv接到结果集的这一步里面去了。它在里面不停的调用KeyValueHeap的next方法,match的调用正好在这个方法。我们现在就去追踪这遗失的部分。
我们直接去看它的match方法就好了,别的不用看了,它处理的情况好多好多,尼玛,这是要死人的节奏啊。
ScanQueryMatcher是用来处理一行数据之间的版本问题的,在每遇到一个新的row的时候,它都会先被设置matcher.setRow(row, offset, length)。
if (limit < 0 || matcher.row == null || !Bytes.equals(row, offset, length, matcher.row,
matcher.rowOffset, matcher.rowLength)) {
this.countPerRow = 0;
matcher.setRow(row, offset, length);
}
上面这段代码在StoreScanner的next方法里面,每当一行结束之后,都会调用这个方法。
在讲match方法之前,我先讲一下rowkey的排序规则,rowkey 正序->family 正序->qualifier 正序->ts 降序->type 降序,那么对于同一个行、列族、列的数据,时间越近的排在前面,类型越大的排在前面,比如Delete就在Put前面,下面是它的类型表。
//search用 Minimum((byte)0), Put((byte)4), Delete((byte)8), DeleteFamilyVersion((byte)10), DeleteColumn((byte)12), DeleteFamily((byte)14), //search用 Maximum((byte)255);
为什么这里先KeyValue的排序规则呢,这当然有关系了,这关系着扫描的时候,谁先谁后的问题,如果时间戳小的在前面,下面这个过滤就不生效了。
下面我们看看它的match方法的检查规则。
1、和当前行比较
//和当前的行进行比较,只有相等才继续,大于当前的行就要跳到下一行,小于说明有问题,停止
int ret = this.rowComparator.compareRows(row, this.rowOffset, this.rowLength,
bytes, offset, rowLength);
if (ret <= -1) {
return MatchCode.DONE;
} else if (ret >= 1) {
return MatchCode.SEEK_NEXT_ROW;
}
2、检查是否所有列都查过了
//所有的列都扫描过来
if (this.columns.done()) {
stickyNextRow = true;
return MatchCode.SEEK_NEXT_ROW;
}
3、检查列的时间戳是否过期
long timestamp = kv.getTimestamp();
// 检查列的时间是否过期
if (columns.isDone(timestamp)) {
return columns.getNextRowOrNextColumn(bytes, offset, qualLength);
}
4a、如果是Delete的类型,加到ScanDeleteTraker。
if (kv.isDelete()) {
this.deletes.add(bytes, offset, qualLength, timestamp, type);
}
4b、如果不是,如果ScanDeleteTraker里面有Delete,就要让它经历ScanDeleteTraker的检验了(进宫前先验一下身)
DeleteResult deleteResult = deletes.isDeleted(bytes, offset, qualLength,
timestamp);
switch (deleteResult) {
case FAMILY_DELETED:
case COLUMN_DELETED:
return columns.getNextRowOrNextColumn(bytes, offset, qualLength);
case VERSION_DELETED:
case FAMILY_VERSION_DELETED:
return MatchCode.SKIP;
case NOT_DELETED:
break;
default:
throw new RuntimeException("UNEXPECTED");
}
这里就要说一下刚才那几个Delete的了:
1)DeleteFamily是最凶狠的,生命周期也长,整个列族全删,基本上会一直存在
2)DeleteColum只删掉一个列,出现这个列的都会被干掉
3)DeleteFamilyVersion没遇到过
4)Delete最差劲儿了,只能删除指定时间戳的,时间戳一定要对哦,否则一旦发现不对的,这个Delete就失效了,可以说,生命周期只有一次,下面是源代码。
public DeleteResult isDeleted(byte [] buffer, int qualifierOffset,
int qualifierLength, long timestamp) {
//时间戳小于删除列族的时间戳,说明这个列族被删掉是后来的事情
if (hasFamilyStamp && timestamp <= familyStamp) {
return DeleteResult.FAMILY_DELETED;
}
//检查时间戳
if (familyVersionStamps.contains(Long.valueOf(timestamp))) {
return DeleteResult.FAMILY_VERSION_DELETED;
}
if (deleteBuffer != null) {
int ret = Bytes.compareTo(deleteBuffer, deleteOffset, deleteLength,
buffer, qualifierOffset, qualifierLength);
if (ret == 0) {
if (deleteType == KeyValue.Type.DeleteColumn.getCode()) {
return DeleteResult.COLUMN_DELETED;
}
// 坑爹的Delete它只删除相同时间戳的,遇到不想的它就pass了
if (timestamp == deleteTimestamp) {
return DeleteResult.VERSION_DELETED;
}
//时间戳不对,这个Delete失效了
deleteBuffer = null;
} else if(ret < 0){
// row比当前的大,这个Delete也失效了
deleteBuffer = null;
} else {
throw new IllegalStateException(...);
}
}
return DeleteResult.NOT_DELETED;
上一章说过,Delete new出来之后什么都不设置,就是DeleteFamily级别的选手,所以在它之后的会全部被干掉,所以你们懂的,我们也会用DeleteColum来删除某一列数据,只要时间戳在它之前的kv就会被干掉,删某个指定版本的少,因为你得知道具体的时间戳,否则你删不了。
例子详解DeleteFamily
假设我们有这些数据
KeyValue [] kvs1 = new KeyValue[] {
KeyValueTestUtil.create("R1", "cf", "a", now, KeyValue.Type.Put, "dont-care"),
KeyValueTestUtil.create("R1", "cf", "a", now, KeyValue.Type.DeleteFamily, "dont-care"),
KeyValueTestUtil.create("R1", "cf", "a", now-500, KeyValue.Type.Put, "dont-care"),
KeyValueTestUtil.create("R1", "cf", "a", now+500, KeyValue.Type.Put, "dont-care"),
KeyValueTestUtil.create("R1", "cf", "a", now, KeyValue.Type.Put, "dont-care"),
KeyValueTestUtil.create("R2", "cf", "z", now, KeyValue.Type.Put, "dont-care")
};
Scan的参数是这些。
Scan scanSpec = new Scan(Bytes.toBytes("R1"));
scanSpec.setMaxVersions(3);scanSpec.setBatch(10);
StoreScanner scan = new StoreScanner(scanSpec, scanInfo, scanType, getCols("a","z"), scanners);
然后,我们先将他们排好序,是这样的。
R1/cf:a/1400602376242(now+500)/Put/vlen=9/mvcc=0, R1/cf:a/1400602375742(now)/DeleteFamily/vlen=9/mvcc=0, R1/cf:a/1400602375742(now)/Put/vlen=9/mvcc=0, R1/cf:a/1400602375742(now)/Put/vlen=9/mvcc=0, R1/cf:a/1400602375242(now-500)/Put/vlen=9/mvcc=0, R2/cf:z/1400602375742(now)/Put/vlen=9/mvcc=0
所以到最后,黄色的三行会被删除,只剩下第一行和最后一行,但是最后一行也会被排除掉,因为它已经换行了,不是同一个行的,不在这一轮进行比较,返回MatchCode.DONE。
---->回到前面是match过程
5、检查时间戳,即设置给Scan的时间戳,这个估计一般很少设置,时间戳过期,就返回下一个MatchCode.SEEK_NEXT_ROW。
6、检查列是否是Scan里面设置的需要查询的列。
7、检查列的版本,Scan设置的MaxVersion,超过了这个version就要赶紧闪人了哈,返回MatchCode.SEEK_NEXT_COL。
对于match的结果,有几个常见的:
1、MatchCode.INCLUDE_AND_SEEK_NEXT_COL 包括当前这个,跳到下一列,会引发StoreScanner的reseek方法。
2、MatchCode.SKIP 忽略掉,继续调用next方法。
3、MatchCode.SEEK_NEXT_ROW 不包括当前这个,继续调用next方法。
4、MatchCode.SEEK_NEXT_COL 不包括它,跳过下一列,会引发StoreScanner的reseek方法。
5、MatchCode.DONE rowkey变了,要留到下次进行比较了
讲到这里基本算结束了。
关于测试
呵呵,有兴趣测试的童鞋可以打开下hbase源码,找到TestStoreScanner这个类自己调试看下结果。
hbase源码系列(十二)Get、Scan在服务端是如何处理?的更多相关文章
- hbase源码系列(二)HTable 探秘
hbase的源码终于搞一个段落了,在接下来的一个月,着重于把看过的源码提炼一下,对一些有意思的主题进行分享一下.继上一篇讲了负载均衡之后,这一篇我们从client开始讲吧,从client到master ...
- 框架源码系列十二:Mybatis源码之手写Mybatis
一.需求分析 1.Mybatis是什么? 一个半自动化的orm框架(Object Relation Mapping). 2.Mybatis完成什么工作? 在面向对象编程中,我们操作的都是对象,Myba ...
- hbase源码系列(十二)Get、Scan在服务端是如何处理
hbase源码系列(十二)Get.Scan在服务端是如何处理? 继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...
- hbase源码系列(十五)终结篇&Scan续集-->如何查询出来下一个KeyValue
这是这个系列的最后一篇了,实在没精力写了,本来还想写一下hbck的,这个东西很常用,当hbase的Meta表出现错误的时候,它能够帮助我们进行修复,无奈看到3000多行的代码时,退却了,原谅我这点自私 ...
- 10 hbase源码系列(十)HLog与日志恢复
hbase源码系列(十)HLog与日志恢复 HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢 ...
- 11 hbase源码系列(十一)Put、Delete在服务端是如何处理
hbase源码系列(十一)Put.Delete在服务端是如何处理? 在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...
- HBase源码系列之HFile
本文讨论0.98版本的hbase里v2版本.其实对于HFile能有一个大体的较深入理解是在我去查看"到底是不是一条记录不能垮block"的时候突然意识到的. 首先说一个对HFile ...
- 9 hbase源码系列(九)StoreFile存储格式
hbase源码系列(九)StoreFile存储格式 从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去 ...
- OpenJDK源码研究笔记(十二):JDBC中的元数据,数据库元数据(DatabaseMetaData),参数元数据(ParameterMetaData),结果集元数据(ResultSetMetaDa
元数据最本质.最抽象的定义为:data about data (关于数据的数据).它是一种广泛存在的现象,在许多领域有其具体的定义和应用. JDBC中的元数据,有数据库元数据(DatabaseMeta ...
- SpringCloud 源码系列(6)—— 声明式服务调用 Feign
SpringCloud 源码系列(1)-- 注册中心 Eureka(上) SpringCloud 源码系列(2)-- 注册中心 Eureka(中) SpringCloud 源码系列(3)-- 注册中心 ...
随机推荐
- GANS 资料
https://blog.csdn.net/a312863063/article/details/83512870 目 录第一章 初步了解GANs 3 1. 生成模型与判别模型. 3 2. 对抗网络思 ...
- python 保留两位小数
>>> a = 1 >>> b = 3 >>> print(a/b) 0 >>> #方法一: ... print(round(a ...
- Tomcat7环境下面MySQL 56/Oracle数据库连接池的配置
环境: Tomcat7 MySQL56 mysql-connector-java-3.1.10-bin.jar 步骤: 1. 配置context.xml 此文件位于~\tomcat7\conf目录下面 ...
- tensorflow省钱方案-ml-engine
google cloud有专门的ml-engine(machine learning engine)模块,可以直接用来跑tensorflow,不用像虚拟机一样开关机.只需要根据需要指定配置就行.收费分 ...
- Shader中ColorMask的使用
ColorMask可以对输出颜色进行Mask处理 使用方法和Cull这些标记差不多 SubShader { ColorMask R Cull Off .... 如果ColorMask填0就什么都不显示
- 深入理解Linux内核-信号
信号:1.最初被引入作为用户态进程间通信2.内核也使用信号通知进程系统所发生的事件3.信号很短,发送给进程的唯一信息通常是一个数.4.名称通常以SIG为前缀5.信号时可消费资源,每个信号只能被传递一次 ...
- 深入理解Linux内核-内核同步
内核基本的同步机制: 抢占内核的主要特点:一个在内核态运行的进程,可能在执行内核函数期间被另外一个进程取代. 内核抢占:Linux 2.6允许用户在编译内核的时候配置十分启用 进程临界区:每个进程中访 ...
- FLINK SQL Calcite原理
http://wuchong.me/blog/2017/03/30/flink-internals-table-and-sql-api/ https://cloud.tencent.com/devel ...
- Fetch API 接口参考
前言 Fetch API是新的ajax解决方案,用于解决古老的XHR对象不能实现的问题,Fetch API 提供了一个获取资源的接口(包括跨域请求),任何使用过 XMLHttpRequest 的人都能 ...
- C++11 constexpr使用
C++11为了提高代码执行效率做了一些改善.这种改善之一就是:生成常量表达式,允许程序利用编译时的计算能力.假如你熟悉模板元编程,你将发现constexpr使这一切变得更加简单.constexpr使我 ...