源码版本:redis-4.0.1
源码位置:

  • dict.hdictEntry、dictht、dict等数据结构定义。
  • dict.c:创建、插入、查找等功能实现。

一、dict 简介

dict (dictionary 字典),通常的存储结构是Key-Value形式的,通过Hash函数对key求Hash值来确定Value的位置,因此也叫Hash表,是一种用来解决算法中查找问题的数据结构,默认的算法复杂度接近O(1),Redis本身也叫REmote DIctionary Server (远程字典服务器),其实也就是一个大字典,它的key通常来说是String类型的,但是Value可以是
String、Set、ZSet、Hash、List等不同的类型,下面我们看下dict的数据结构定义。

二、数据结构定义

与dict相关的关键数据结构有三个,分别是:

  • dictEntry 表示一个Key-Value节点。
  • dictht表示一个Hash表。
  • dict是Redis中的字典结构,包含两个dictht

dictEntry结构的代码如下:

  1. typedef struct dictEntry {
  2. void *key; //key void*表示任意类型指针
  3. union { //联合体中对于数字类型提供了专门的类型优化
  4. void *val;
  5. uint64_t u64;
  6. int64_t s64;
  7. } v;
  8. struct dictEntry *next; //next指针
  9. } dictEntry;

dictht的代码如下:

  1. typedef struct dictht {
  2. dictEntry **table; //数组指针,每个元素都是一个指向dictEntry的指针
  3. unsigned long size; //表示这个dictht已经分配空间的大小,大小总是2^n
  4. unsigned long sizemask; //sizemask = size - 1; 是用来求hash值的掩码,为2^n-1
  5. unsigned long used; //目前已有的元素数量
  6. } dictht;

最后是真正的dict结构:

  1. typedef struct dict {
  2. dictType *type; //type中定义了对于Hash表的操作函数,比如Hash函数,key比较函数等
  3. void *privdata; //privdata是可以传递给dict的私有数据
  4. dictht ht[2]; //每一个dict都包含两个dictht,一个用于rehash
  5. int rehashidx; //表示此时是否在进行rehash操作
  6. int iterators; //迭代器
  7. } dict;

其实通过上面的三个数据结构,已经可以大概看出dict的组成,数据(Key-Value)存储在每一个dictEntry节点;然后一条Hash表就是一个dictht结构,里面标明了Hash表的size,used等信息;最后每一个Redis的dict结构都会默认包含两个dictht,如果有一个Hash表满足特定条件需要扩容,则会申请另一个Hash表,然后把元素ReHash过来,ReHash的意思就是重新计算每个Key的Hash值,然后把它存放在第二个Hash表合适的位置,但是这个操作在Redis中并不是集中式一次完成的,而是在后续的增删改查过程中逐步完成的,这个叫渐进式ReHash,我们后文会专门讨论。

三、创建、插入、键冲突、扩张

下面我们跟随一个例子来看有关dict的创建,插入,键冲突的解决办法以及扩张的问题。在这里推荐一个有关调试Redis数据结构代码的方法:下载一份Redis源码,然后直接把server.cmain函数注释掉,加入自己的代码,直接make之后就可以跑了。我们的例子如下所示:

  1. int main(int argc, char **argv) {
  2. int ret;
  3. sds key = sdsnew("key");
  4. sds val = sdsnew("val");
  5. dict *dd = dictCreate(&keyptrDictType, NULL);
  6. printf("Add elements to dict\n");
  7. for (int i = 0; i < 6 ; ++i) {
  8. ret = dictAdd(dd, sdscatprintf(key, "%d", i), sdscatprintf(val, "%d", i));
  9. printf("Add ret%d is :%d ,", i, ret);
  10. printf("ht[0].used :%lu, ht[0].size :%lu, "
  11. "ht[1].used :%lu, ht[1].size :%lu\n", dd->ht[0].used, dd->ht[0].size, dd->ht[1].used, dd->ht[1].size);
  12. }
  13. printf("\nDel elements to dict\n");
  14. for (int i = 0; i < 6 ; ++i) {
  15. ret = dictDelete(dd, sdscatprintf(key, "%d", i));
  16. printf("Del ret%d is :%d ,", i, ret);
  17. printf("ht[0].used :%lu, ht[0].size :%lu, "
  18. "ht[1].used :%lu, ht[1].size :%lu\n", dd->ht[0].used, dd->ht[0].size, dd->ht[1].used, dd->ht[1].size);
  19. }
  20. sdsfree(key);
  21. sdsfree(val);
  22. dictRelease(dd);
  23. return 0;
  24. }
  25. Out >
  26. Add elements to dict
  27. Add ret0 is :0 ,ht[0].used :1, ht[0].size :4, ht[1].used :0, ht[1].size :0
  28. Add ret1 is :0 ,ht[0].used :2, ht[0].size :4, ht[1].used :0, ht[1].size :0
  29. Add ret2 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :0, ht[1].size :0
  30. Add ret3 is :0 ,ht[0].used :4, ht[0].size :4, ht[1].used :0, ht[1].size :0
  31. Add ret4 is :0 ,ht[0].used :4, ht[0].size :4, ht[1].used :1, ht[1].size :8
  32. Add ret5 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :3, ht[1].size :8
  33. Del elements to dict
  34. Del ret0 is :0 ,ht[0].used :5, ht[0].size :8, ht[1].used :0, ht[1].size :0
  35. Del ret1 is :0 ,ht[0].used :4, ht[0].size :8, ht[1].used :0, ht[1].size :0
  36. Del ret2 is :0 ,ht[0].used :3, ht[0].size :8, ht[1].used :0, ht[1].size :0
  37. Del ret3 is :0 ,ht[0].used :2, ht[0].size :8, ht[1].used :0, ht[1].size :0
  38. Del ret4 is :0 ,ht[0].used :1, ht[0].size :8, ht[1].used :0, ht[1].size :0
  39. Del ret5 is :0 ,ht[0].used :0, ht[0].size :8, ht[1].used :0, ht[1].size :0
  • dict *dd = dictCreate(&keyptrDictType, NULL); 创建了一个名为dd,type为keyptrDictType的dict,创建代码如下,需要注意的是这个操作只给dict本身申请了空间,但是像dict->ht->table这些数据存储节点并没有分配空间,这些空间是dictAdd的时候才分配的。
  1. /* Create a new hash table */
  2. dict *dictCreate(dictType *type,
  3. void *privDataPtr)
  4. {
  5. dict *d = zmalloc(sizeof(*d)); //申请空间,sizeof(*d)为88个字节
  6. _dictInit(d,type,privDataPtr); //一些置NULL操作,type和privdata置为参数指定值
  7. return d;
  8. }
  • ret = dictAdd(dd, sdscatprintf(key, "%d", i), sdscatprintf(val, "%d", i)); 接着我们定义了两个sds,并且for循环分别将他们dictAdd,来看下dictAdd的代码,它实际上调用了dictAddRaw函数:
  1. dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
  2. {
  3. int index;
  4. dictEntry *entry;
  5. dictht *ht;
  6. if (dictIsRehashing(d)) _dictRehashStep(d);
  7. /* Get the index of the new element, or -1 if
  8. * the element already exists. */
  9. if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
  10. return NULL;
  11. /* Allocate the memory and store the new entry.
  12. * Insert the element in top, with the assumption that in a database
  13. * system it is more likely that recently added entries are accessed
  14. * more frequently. */
  15. ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
  16. entry = zmalloc(sizeof(*entry));
  17. entry->next = ht->table[index];
  18. ht->table[index] = entry;
  19. ht->used++;
  20. /* Set the hash entry fields. */
  21. dictSetKey(d, entry, key);
  22. return entry;
  23. }

可以看到首先检测是否在进行ReHash(我们先跳过ReHash这个概念),接下来算出了一个index值,然后根据是否在进行ReHash选择了其中一个dt(0或者1),之后进行了头插,而且英文注释中也写的很清楚将数据插在头部基于数据库系统总是会经常访问最近添加的节点,然后将key设置之后就返回了,但是我们貌似还是没有发现申请空间的函数,其实是在算index的时候_dictKeyIndex()会自动判断,如下:

  1. static int _dictKeyIndex(dict *d, const void *key, unsigned int hash, dictEntry **existing)
  2. {
  3. unsigned int idx, table;
  4. dictEntry *he;
  5. if (existing) *existing = NULL;
  6. /* Expand the hash table if needed */
  7. if (_dictExpandIfNeeded(d) == DICT_ERR)
  8. return -1;
  9. for (table = 0; table <= 1; table++) {
  10. idx = hash & d->ht[table].sizemask;
  11. /* Search if this slot does not already contain the given key */
  12. he = d->ht[table].table[idx];
  13. while(he) {
  14. if (key==he->key || dictCompareKeys(d, key, he->key)) {
  15. if (existing) *existing = he;
  16. return -1;
  17. }
  18. he = he->next;
  19. }
  20. if (!dictIsRehashing(d)) break;
  21. }
  22. return idx;
  23. }

_dictExpandIfNeeded(d)进行空间判断,如果还未申请,就创建默认大小,其中它里面也有dict扩容的策略(见注释):

  1. static int _dictExpandIfNeeded(dict *d)
  2. {
  3. /* Incremental rehashing already in progress. Return. */
  4. if (dictIsRehashing(d)) return DICT_OK;
  5. //如果正在ReHash,那直接返回OK,其实也表明申请了空间不久。
  6. /* If the hash table is empty expand it to the initial size. */
  7. if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
  8. //如果 0 号哈希表的大小为0,表示还未创建,按照默认大小`DICT_HT_INITIAL_SIZE=4`去创建
  9. /* If we reached the 1:1 ratio, and we are allowed to resize the hash
  10. * table (global setting) or we should avoid it but the ratio between
  11. * elements/buckets is over the "safe" threshold, we resize doubling
  12. * the number of buckets. */
  13. //如果满足 0 号哈希表used>size &&(dict_can_resize为1 或者 used/size > 5) 那就默认扩两倍大小
  14. if (d->ht[0].used >= d->ht[0].size &&
  15. (dict_can_resize ||
  16. d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
  17. {
  18. return dictExpand(d, d->ht[0].used*2);
  19. }
  20. return DICT_OK;
  21. }

对于我们的代码,走的是if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);这个分支,也就是会去创建一个dictht的table大小为4的dict,如下:

  1. int dictExpand(dict *d, unsigned long size)
  2. {
  3. dictht n; /* the new hash table */
  4. unsigned long realsize = _dictNextPower(size);
  5. /* the size is invalid if it is smaller than the number of
  6. * elements already inside the hash table */
  7. if (dictIsRehashing(d) || d->ht[0].used > size)
  8. return DICT_ERR;
  9. /* Rehashing to the same table size is not useful. */
  10. if (realsize == d->ht[0].size) return DICT_ERR;
  11. /* Allocate the new hash table and initialize all pointers to NULL */
  12. n.size = realsize;
  13. n.sizemask = realsize-1;
  14. n.table = zcalloc(realsize*sizeof(dictEntry*));
  15. n.used = 0;
  16. /* Is this the first initialization? If so it's not really a rehashing
  17. * we just set the first hash table so that it can accept keys. */
  18. if (d->ht[0].table == NULL) {
  19. d->ht[0] = n;
  20. return DICT_OK;
  21. }
  22. /* Prepare a second hash table for incremental rehashing */
  23. d->ht[1] = n;
  24. d->rehashidx = 0;
  25. return DICT_OK;
  26. }

需要注意的是_dictNextPower可以计算出距离size最近,且大于或者等于size的2的次方的值,比如size是4,那距离其最近的值为4(2的平方),size是6,距离其最近的值为8(2的三次方),然后申请空间,之后判断如果d->ht[0].table == NULL也就是我们目前的还未初始化的情况,则初始化 0 号Hash表,之后添加相应的元素,我们程序的输出如下所示:

  1. Add ret0 is :0 ,ht[0].used :1, ht[0].size :4, ht[1].used :0, ht[1].size :0

如果图示目前的Hash表,如下所示:

  • 接下来for循环继续添加,当i = 4时,也就是当添加第5个元素时,默认初始化大小为4的Hash表已经不够用了。此时的used=4,我们看看扩张操作发生了什么,代码从_dictExpandIfNeeded(d)说起,此时满足条件,会执行扩张操作,如下:
  1. if (d->ht[0].used >= d->ht[0].size &&
  2. (dict_can_resize ||
  3. d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
  4. {
  5. return dictExpand(d, d->ht[0].used*2);
  6. }

dictExpand(d, d->ht[0].used*2); 表示重新申请了一个大小为之前2倍的Hash表,即 1 号Hash表。然后将d->rehashidx = 0;即表明此时开始ReHash操作。

Rehash就是将原始Hash表(0号Hash表)上的Key重新按照Hash函数计算Hash值,存到新的Hash表(1号Hash表)的过程。

这一步执行之后此时Hash表如下所示:

由图可以看到 0 号Hash表已经满了,此时我们的新数据被存到了 1 号哈希表中,接下来我们开始了第6次循环,我们继续看在ReHash的情况下数据是如何存入的,也就是第6次循环,即添加key5的过程,继续调用dictAddRaw函数:

  1. if (dictIsRehashing(d)) _dictRehashStep(d);

此时因为d->rehashidx = 0,所以会执行渐进式Hash操作,即_dictRehashStep(d):

  1. static void _dictRehashStep(dict *d) {
  2. if (d->iterators == 0) dictRehash(d,1); //如果迭代器是0,ReHash步长为1
  3. }
  4. int dictRehash(dict *d, int n) {
  5. int empty_visits = n*10; /* Max number of empty buckets to visit. */
  6. if (!dictIsRehashing(d)) return 0;
  7. while(n-- && d->ht[0].used != 0) {
  8. dictEntry *de, *nextde;
  9. /* Note that rehashidx can't overflow as we are sure there are more
  10. * elements because ht[0].used != 0 */
  11. assert(d->ht[0].size > (unsigned long)d->rehashidx);
  12. while(d->ht[0].table[d->rehashidx] == NULL) {
  13. d->rehashidx++;
  14. if (--empty_visits == 0) return 1;
  15. }
  16. de = d->ht[0].table[d->rehashidx];
  17. /* Move all the keys in this bucket from the old to the new hash HT */
  18. while(de) {
  19. unsigned int h;
  20. nextde = de->next;
  21. /* Get the index in the new hash table */
  22. h = dictHashKey(d, de->key) & d->ht[1].sizemask;
  23. de->next = d->ht[1].table[h];
  24. d->ht[1].table[h] = de;
  25. d->ht[0].used--;
  26. d->ht[1].used++;
  27. de = nextde;
  28. }
  29. d->ht[0].table[d->rehashidx] = NULL;
  30. d->rehashidx++;
  31. }
  32. /* Check if we already rehashed the whole table... */
  33. if (d->ht[0].used == 0) {
  34. zfree(d->ht[0].table);
  35. d->ht[0] = d->ht[1];
  36. _dictReset(&d->ht[1]);
  37. d->rehashidx = -1;
  38. return 0;
  39. }
  40. /* More to rehash... */
  41. return 1;
  42. }

int empty_visits = n*10; empty_visits表示每次最多跳过10倍步长的空桶(一个桶就是ht->table数组的一个位置),然后当我们找到一个非空的桶时,就将这个桶中所有的key全都ReHash到 1 号Hash表。最后每次都会判断是否将所有的key全部ReHash了,如果已经全部完成,就释放掉ht[0],然后将ht[1]变成ht[0]。

也就是此次dictAdd操作不仅将key5添加进去,还将 0 号Hash表中2号桶中的key0 ReHash到了 1 号Hash表上。所以此时的 2 号Hash表上有3个元素,如下:

  1. Add ret5 is :0 ,ht[0].used :3, ht[0].size :4, ht[1].used :3, ht[1].size :8

图示结果如下所示:

  • 接下来我们的程序执行了删除操作,dictDelete函数,实际上调用的是dictGenericDelete函数。
  1. static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
  2. unsigned int h, idx;
  3. dictEntry *he, *prevHe;
  4. int table;
  5. if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;
  6. if (dictIsRehashing(d)) _dictRehashStep(d);
  7. h = dictHashKey(d, key);
  8. for (table = 0; table <= 1; table++) {
  9. idx = h & d->ht[table].sizemask;
  10. he = d->ht[table].table[idx];
  11. prevHe = NULL;
  12. while(he) {
  13. if (key==he->key || dictCompareKeys(d, key, he->key)) {
  14. /* Unlink the element from the list */
  15. if (prevHe)
  16. prevHe->next = he->next;
  17. else
  18. d->ht[table].table[idx] = he->next;
  19. if (!nofree) {
  20. dictFreeKey(d, he);
  21. dictFreeVal(d, he);
  22. zfree(he);
  23. }
  24. d->ht[table].used--;
  25. return he;
  26. }
  27. prevHe = he;
  28. he = he->next;
  29. }
  30. if (!dictIsRehashing(d)) break;
  31. }
  32. return NULL; /* not found */
  33. }
  • if (dictIsRehashing(d)) _dictRehashStep(d); 实际上也执行了ReHash步骤,这次将 0 号哈希表上的剩余3个key全部ReHash到了 1 号哈希表上,这其实就是渐进式ReHash了,因为ReHash操作不是一次性、集中式完成的,而是多次进行,分散在增删改查中,这就是渐进式ReHash的思想。

渐进式ReHash是指ReHash操作不是一次集中式完成的,对于Redis来说,如果Hash表的key太多,这样可能导致ReHash操作需要长时间进行,阻塞服务器,所以Redis本身将ReHash操作分散在了后续的每次增删改查中。

说到这里,我有个问题:虽然渐进式ReHash分散了ReHash带来的问题,但是带来的问题是对于每次增删改查的时间可能是不稳定的,因为每次增删改查可能就需要带着ReHash操作,所以可不可以fork一个子进程去做这个事情呢?

  • 继续看代码,接下来通过h = dictHashKey(d, key);计算出index,然后根据有无进行ReHash确定遍历2个Hash表还是一个Hash表。因为ReHash操作如果在进行的话,key不确定存在哪个Hash表中,没有被ReHash的话就在0号,否则就在1号。

  • 这次Delete操作成功删除了key0,而且将 0 号哈希表上的剩余3个key全部ReHash到了 1 号哈希表上,并且因为ReHash结束,所以将1号Hash表变成了0号哈希表,如图所示:

  • 后续的删除操作清除了所有的key,然后我们调用了dictRelease(dd)释放了这个字典。
  1. void dictRelease(dict *d)
  2. {
  3. _dictClear(d,&d->ht[0],NULL);
  4. _dictClear(d,&d->ht[1],NULL);
  5. zfree(d);
  6. }
  7. int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {
  8. unsigned long i;
  9. /* Free all the elements */
  10. for (i = 0; i < ht->size && ht->used > 0; i++) {
  11. dictEntry *he, *nextHe;
  12. if (callback && (i & 65535) == 0) callback(d->privdata);
  13. if ((he = ht->table[i]) == NULL) continue;
  14. while(he) {
  15. nextHe = he->next;
  16. dictFreeKey(d, he);
  17. dictFreeVal(d, he);
  18. zfree(he);
  19. ht->used--;
  20. he = nextHe;
  21. }
  22. }
  23. /* Free the table and the allocated cache structure */
  24. zfree(ht->table);
  25. /* Re-initialize the table */
  26. _dictReset(ht);
  27. return DICT_OK; /* never fails */
  28. }

四、ReHash和渐进式ReHash

  • Rehash:就是将原始Hash表(0号Hash表)上的Key重新按照Hash函数计算Hash值,存到新的Hash表(1号Hash表)的过程。
  • 渐进式ReHash:是指ReHash操作不是一次性、集中式完成的,对于Redis来说,如果Hash表的key太多,这样可能导致ReHash操作需要长时间进行,阻塞服务器,所以Redis本身将ReHash操作分散在了后续的每次增删改查中。

具体情况看上面例子。

五、ReHash期间访问策略

Redis中默认有关Hash表的访问操作都会先去 0 号哈希表查找,然后根据是否正在ReHash决定是否需要去 1 号Hash表中查找,关键代码如下(dict.c->dictFind()):

  1. for (table = 0; table <= 1; table++) {
  2. idx = h & d->ht[table].sizemask;
  3. he = d->ht[table].table[idx];
  4. while(he) {
  5. if (key==he->key || dictCompareKeys(d, key, he->key))
  6. return he;
  7. he = he->next;
  8. }
  9. if (!dictIsRehashing(d)) return NULL; //根据这一句判断是否需要在 1 号哈希表中查找。
  10. }

五、遍历

可以使用dictNext函数遍历:

  1. dictIterator *i = dictGetIterator(dd); //获取迭代器
  2. dictEntry *de;
  3. while ((de = dictNext(i)) != NULL) { //只要结尾不为NULL,就继续遍历
  4. printf("%s->%s\n",(char*)de->key, (char*)de->v.val);
  5. }
  6. Out >
  7. key3->val3
  8. key2->val2
  9. key1->val1
  10. key5->val5
  11. key0->val0
  12. key4->val4

有关遍历函数dictSacn()的算法,也是个比较难的话题,有时间再看吧。

六、总结

这篇文章主要分析了dict的数据结构、创建、扩容、ReHash、渐进式ReHash,删除等机制。只是单纯的数据结构的分析,没有和Redis一些机制进行结合映射,这方面后续再补充,但是已经是一篇深度好文了 :)。

[完]

Redis源码分析(dict)的更多相关文章

  1. Redis源码分析:serverCron - redis源码笔记

    [redis源码分析]http://blog.csdn.net/column/details/redis-source.html   Redis源代码重要目录 dict.c:也是很重要的两个文件,主要 ...

  2. redis源码分析之事务Transaction(下)

    接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需 ...

  3. redis源码分析之有序集SortedSet

    有序集SortedSet算是redis中一个很有特色的数据结构,通过这篇文章来总结一下这块知识点. 原文地址:http://www.jianshu.com/p/75ca5a359f9f 一.有序集So ...

  4. Redis源码分析(intset)

    源码版本:4.0.1 源码位置: intset.h:数据结构的定义 intset.c:创建.增删等操作实现 1. 整数集合简介 intset是Redis内存数据结构之一,和之前的 sds. skipl ...

  5. redis源码分析之发布订阅(pub/sub)

    redis算是缓存界的老大哥了,最近做的事情对redis依赖较多,使用了里面的发布订阅功能,事务功能以及SortedSet等数据结构,后面准备好好学习总结一下redis的一些知识点. 原文地址:htt ...

  6. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  7. Redis源码分析系列

    0.前言 Redis目前热门NoSQL内存数据库,代码量不是很大,本系列是本人阅读Redis源码时记录的笔记,由于时间仓促和水平有限,文中难免会有错误之处,欢迎读者指出,共同学习进步,本文使用的Red ...

  8. Redis源码分析(skiplist)

    源码版本: redis-4.0.1 源码位置: server.h :zskiplistNode和zskiplist的数据结构定义. t_zset.c: 以zsl开头的函数是SkipList相关的操作函 ...

  9. redis源码分析

    我阅读的源码版本是redis-2.8.19 src目录下总共96个.h,.c文件 1. 数据结构相关源码(15个左右)字符串代码: sds.h, sds.c字典:dict.h, dict.c链表: a ...

随机推荐

  1. spy++查找窗口句柄

    spy++可以用来查找桌面程序(c/s)的窗口句柄,实现自动化测试. def find_idxSubHandle(pHandle, winClass, index=0): ""&q ...

  2. 关于Postman你必须学会的技能

    关于Postman 工欲善其事,必先利其器,在了解了接口测试之后,就要选择一款适用的工具.之所以选择postman是因为它简单.容易上手.能覆盖大多数HTTP接口测试场景,性价比极高. Postman ...

  3. Mybatis逆向工程和新版本MybatisPlus3.4逆向工程的使用

    Mybatis和MybatisPlus3.4的使用 目录 Mybatis和MybatisPlus3.4的使用 1 RESTFUL 2 逆向工程 2.1 tkMybatis逆向工程 2.1.1 导入依赖 ...

  4. NOIP 模拟六 考试总结

    T1辣鸡 T1就搞得这莫不愉快.. 大致题意是给你几个矩形,矩形覆盖的点都标记上,每个矩形无重复部分,求满足(x,y) (x+1,y+1)都标记过的点对数,范围1e9. 看起来很牛的样子,我确实也被1 ...

  5. display:none、visibility:hidden,opacity:0三者区别

    1. display:none 设置display:none,让这个元素消失 消失不占据原本任何位置 连带子元素一起消失 元素显示:display:block 2. visibility:hidden ...

  6. SpringPlugin-Core在业务中的应用

    前言 一直负责部门的订单模块,从php转到Java也是如此,换了一种语言来实现订单相关功能.那么Spring里有很多已经搭建好基础模块的设计模式来帮助我们解耦实际业务中的逻辑,用起来非常的方便!就比如 ...

  7. Linear Referencing Tools(线性参考工具)

    线性参考工具 # Process: 创建路径 arcpy.CreateRoutes_lr("", "", 输出路径要素类, "LENGTH" ...

  8. Miller-Rabin and Pollard-Rho

    实话实说,我自学(肝)了两天才学会这两个随机算法 记录: Miller-Rabin 她是一个素数判定的算法. 首先需要知道费马小定理 \[a^{p-1}\equiv1\pmod{p}\quad p\i ...

  9. 虚拟机Parallels Desktop 17 (PD17)支持M1 自己动手制作启动器解锁

    个人博客:xzajyjs.cn 如果自己有能力的话,直接查看这个视频即可.点此 前段时间刚出pd17,作为mac上最最强(没有之一)的虚拟机,版本17更是更进一步,性能提升极大,更是支持了Monter ...

  10. Java只有值传递

    二哥,好久没更新面试官系列的文章了啊,真的是把我等着急了,所以特意过来催催.我最近一段时间在找工作,能从二哥的文章中学到一点就多一点信心啊! 说句实在话,离读者 trust you 发给我这段信息已经 ...