在《Simple Dynamic Strings(SDS)源码解析和使用说明一》文中,我们分析了SDS库中数据的基本结构和创建、释放等方法。本文将介绍其一些其他方法及实现。(转载请指明出于breaksoftware的csdn博客)

字符串连接

SDS库提供下面两种方法进行字符串连接

sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);

sdscat函数在底层使用了sdscatlen去实现。sdscatlen方法第一个元素是需要被连接的SDS字符串,第二个参数是需要连接的内容起始地址,第三个是其内容的长度。和C语言中的连接函数不同,sdscatlen方法并不要求追加的内容要以NULL结尾,因为SDS字符串可以在内容中间承载NULL字符。但是sdscat则需要追加的字符串以NULL结尾,因为它没有提供长度参数。我们看下它们的实现:

sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;memcpy(s+curlen, t, len);sdssetlen(s, curlen+len);s[curlen+len] = '\0';return s;
}

sdscatlen方法中通过sdsMakeRoomFor方法获取需要被追加的sds对象,然后通过memcpy追加相关的内容到该对象的字符串末尾。最后修改SDS字符串中代表已经使用了空间长度的字段len,并把最后一位设置为NULL。我们需要关注下sdsMakeRoomFor方法的实现

sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;

sdsMakeRoomFor方法首先需要知道被追加的SDS字符串还有多少空余的空间,这步计算通过sdsavail方法实现,其实现也很简单,我们以SDS_TYPE_5和SDS_TYPE_8为例:

static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}

从这个设计可以看出,作者认为如果调用sdsavail方法时,这个SDS字符串可能是需要扩展空间了。如果此时它的类型是SDS_TYPE_5,则不经过任何计算,直接认为可用空间不够。如果不是空间最小的类型,则通过分配的了空间大小alloc减去已使用的空间大小len计算出还可用的空间大小。

再回到sdsMakeRoomFor方法中,如果判断发现SDS字符串剩余空间的大小足以承载追加的内容,则直接返回入参字符串对象。如果不够,则需要计算需要的长度

    len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;

SDS_MAX_PREALLOC是1M的空间,如果追加的长度和原始长度之和在1M以内,则新的空间是它们和的2倍大;如果大于1M,则在它们之和的基础上增加1M。这就是预分配的逻辑,这样设计可以预防频繁的的内存分配操作,当然相应的也会增加一定的内存浪费。但是总的来说,在目前CPU资源比内存资源贵的场景下,空间换时间还是比较好的。

然后通过新空间的大小匹配SDS字符串类型,如果新老类型相同,则直接使用realloc操作扩展空间。如果类型不同,则需要重新分配一个空间,将老空间里内容复制过来,最后还要将老空间释放掉。

    type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, newlen);return s;
}

sdsMakeRoomFor方法在之后的代码中我们会反复见到,但是见到它就要有个印象:通过它操作的SDS字符串可能是原始的,也可能是将原始空间释放后重新分配的。

我们再看下它们的使用样例:

sds s = sdsempty();
s = sdscat(s, "Hello ");
s = sdscat(s, "World!");
printf("%s\n", s);output> Hello World!

SDS字符串库还提供了连接两个SDS字符串的方法

sds sdscatsds(sds s, const sds t);

其底层还是调用了sdscatlen方法

sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));
}

它的使用方法是:

sds s1 = sdsnew("aaa");
sds s2 = sdsnew("bbb");
s1 = sdscatsds(s1,s2);
sdsfree(s2);
printf("%s\n", s1);output> aaabbb

还有一个特殊的方法,它是用于扩张SDS字符串的长度

sds sdsgrowzero(sds s, size_t len);

如果入参s的长度已经大于等于len了,则不作任何操作;否则增加s的长度到len,并使用NULL去填充多出来的空间。它的实现和sdscatlen很像,只是填充的长度是总长,而不是追加的长度;填充的字符是NULL而已。

sds sdsgrowzero(sds s, size_t len) {size_t curlen = sdslen(s);if (len <= curlen) return s;s = sdsMakeRoomFor(s,len-curlen);if (s == NULL) return NULL;/* Make sure added region doesn't contain garbage */memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */sdssetlen(s, len);return s;
}

我们看下使用例子:

sds s = sdsnew("Hello");
s = sdsgrowzero(s,6);
s[5] = '!'; /* We are sure this is safe because of sdsgrowzero() */
printf("%s\n', s);output> Hello!

字符串格式化

字符串格式化是非常有用的工具,它可以让我们通过指定格式生成一个字符串。SDS字符串库也提供了相应的方法:

sds sdscatprintf(sds s, const char *fmt, ...) {va_list ap;char *t;va_start(ap, fmt);t = sdscatvprintf(s,fmt,ap);va_end(ap);return t;
}

这个方法底层调用了sdscatvprintf方法:

sds sdscatvprintf(sds s, const char *fmt, va_list ap) {va_list cpy;char staticbuf[1024], *buf = staticbuf, *t;size_t buflen = strlen(fmt)*2;/* We try to start using a static buffer for speed.* If not possible we revert to heap allocation. */if (buflen > sizeof(staticbuf)) {buf = s_malloc(buflen);if (buf == NULL) return NULL;} else {buflen = sizeof(staticbuf);}/* Try with buffers two times bigger every time we fail to* fit the string in the current buffer size. */while(1) {buf[buflen-2] = '\0';va_copy(cpy,ap);vsnprintf(buf, buflen, fmt, cpy);va_end(cpy);if (buf[buflen-2] != '\0') {if (buf != staticbuf) s_free(buf);buflen *= 2;buf = s_malloc(buflen);if (buf == NULL) return NULL;continue;}break;}/* Finally concat the obtained string to the SDS string and return it. */t = sdscat(s, buf);if (buf != staticbuf) s_free(buf);return t;
}

这和我们之前使用C语言的格式化不同,这个方法将格式化后的字符串追加到入参第一个参数s的后面。怎么感觉第一个参数s非常鸡肋,我们看看调用的例子就感觉到了:

char *name = "Anna";
int loc = 2500;
sds s;
s = sdscatprintf(sdsempty(), "%s wrote %d lines of LISP\n", name, loc);

我们再看一个使用特例:

int some_integer = 100;
sds num = sdscatprintf(sdsempty(),"%d\n", some_integer);

这个方法将一个整形转换成一个字符串。但是这个方法在此次转换场景中还是非常低效的,我们会在之后介绍专门针对整形数字转换成字符串的高效方法。

整形数组转字符串

sds提供下面这种方法将整形数字数字转换成字符串,当然这个转换的效率要比sdscatprintf要高:

sds sdsfromlonglong(long long value) {char buf[SDS_LLSTR_SIZE];int len = sdsll2str(buf,value);return sdsnewlen(buf,len);
}

我们看下sdsll2str为什么比较高效:

int sdsll2str(char *s, long long value) {char *p, aux;unsigned long long v;size_t l;/* Generate the string representation, this method produces* an reversed string. */v = (value < 0) ? -value : value;p = s;do {*p++ = '0'+(v%10);v /= 10;} while(v);if (value < 0) *p++ = '-';/* Compute length and add null term. */l = p-s;*p = '\0';/* Reverse the string. */p--;while(s < p) {aux = *s;*s = *p;*p = aux;s++;p--;}return l;
}

这个方法底层没有使用字符串格式化这种比较通用但是效率不高的方法,它将入参整形数字从后向前逐个分解出来转换成字符,然后再逆序将这些字符写回到字符串内存空间中。这个函数还可以处理负数,相应的SDS字符串库还提供了针对无符号整形数的转换函数sdsull2str,其实现和sdsll2str非常类似,这儿我就不详细说明了。

字符串裁剪和截取

字符串裁剪操作在实际工作中也是常用的。SDS字符串库通过下面这个方法实现裁剪

sds sdstrim(sds s, const char *cset) {char *start, *end, *sp, *ep;size_t len;sp = start = s;ep = end = s+sdslen(s)-1;while(sp <= end && strchr(cset, *sp)) sp++;while(ep > sp && strchr(cset, *ep)) ep--;len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);s[len] = '\0';sdssetlen(s,len);return s;
}

该函数第二个参数是一个C语言的字符串,其以NULL结尾,包含了需要被裁剪掉的字符。这个裁剪操作通过两个while循环,分别从待裁剪的字符串最前和最后两个位置向另一个方向进行检索,只要遇到需要被裁剪的就继续探索下一个字符,如果是不需要裁剪的就终止当前方向的探测和检索。最终确定剩下的字符串的起始地址后,将这段空间内容复制到SDS字符串内容起始处,并设置结尾符NULL和已使用的空间长度记录变量len。我们来看个使用例子:

sds s = sdsnew("         my string\n\n  ");
sdstrim(s," \n");
printf("-%s-\n",s);output> -my string-

可见my和string之间的空格没有被裁剪,虽然它在要被裁剪的字符串列表中。这儿还有个地方需要说明下,该字符串裁剪操作没有进行字符串空间的再分配,而是利用原来的字符串空间进行处理的。

字符串截取操作是通过指定字符串的前后下标方式,截取其区间的内容并返回。这块操作通过下面方法实现的:

void sdsrange(sds s, int start, int end) {size_t newlen, len = sdslen(s);if (len == 0) return;if (start < 0) {start = len+start;if (start < 0) start = 0;}if (end < 0) {end = len+end;if (end < 0) end = 0;}newlen = (start > end) ? 0 : (end-start)+1;if (newlen != 0) {if (start >= (signed)len) {newlen = 0;} else if (end >= (signed)len) {end = len-1;newlen = (start > end) ? 0 : (end-start)+1;}} else {start = 0;}if (start && newlen) memmove(s, s+start, newlen);s[newlen] = 0;sdssetlen(s,newlen);
}

可见其实现就是简单的下标计算,然后内容复制,最后是结尾符NULL设置和已使用空间长度len字段设置。从代码中我们可以看出,用户传入的下标参数可以是正数,也可以是负数。正数代表下标从起始位置开始,负数代表下标从结束位置开始。这个操作也没有进行内存的重新分配。其使用样例见下:

sds s = sdsnew("Hello World!");
sdsrange(s,6,-1);
printf("-%s-\n");
sdsrange(s,0,-2);
printf("-%s-\n");output> -World!-
output> -World-

字符串复制

字符串复制的操作也非常简单,SDS字符串库提供了两个方法,一个是供C语言字符串使用的

sds sdscpy(sds s, const char *t) {return sdscpylen(s, t, strlen(t));
}

另一个则是可以复制包含NULL字符的二进制数据的

sds sdscpylen(sds s, const char *t, size_t len) {if (sdsalloc(s) < len) {s = sdsMakeRoomFor(s,len-sdslen(s));if (s == NULL) return NULL;}memcpy(s, t, len);s[len] = '\0';sdssetlen(s, len);return s;
}

字符串转可视化

因为SDS字符串中可以包含二进制字符,所以当我们试图打印出这个字符串时,printf方法可能输出不可见的字符。这在调试一一段数据的时候可能比较有用,SDS提供了下面这个方法将字符串中不可见字符和转义字符都转换成可见的内容:

sds sdscatrepr(sds s, const char *p, size_t len) {s = sdscatlen(s,"\"",1);while(len--) {switch(*p) {case '\\':case '"':s = sdscatprintf(s,"\\%c",*p);break;case '\n': s = sdscatlen(s,"\\n",2); break;case '\r': s = sdscatlen(s,"\\r",2); break;case '\t': s = sdscatlen(s,"\\t",2); break;case '\a': s = sdscatlen(s,"\\a",2); break;case '\b': s = sdscatlen(s,"\\b",2); break;default:if (isprint(*p))s = sdscatprintf(s,"%c",*p);elses = sdscatprintf(s,"\\x%02x",(unsigned char)*p);break;}p++;}return sdscatlen(s,"\"",1);
}

这个方法的实现原理也很简单。对于可见字符,则直接显示。对于被反斜杠转义的字符,增加一个反斜杠使得反斜杠自身被转义,从而显示出可打印的内容。对于剩下的不可见的,则将其转成8进制数字输出。我们看下例子:

sds s1 = sdsnew("abcd");
sds s2 = sdsempty();
s[1] = 1;
s[2] = 2;
s[3] = '\n';
s2 = sdscatrepr(s2,s1,sdslen(s1));
printf("%s\n", s2);output> "a\x01\x02\n"

字符串拆分

字符串拆分是指将一个一定规则的字符串,按照某种分隔符进行切分,从而得到一组切分后的字符串的操作。举个例子,我们要对下面这串字符按照|-|为切割符进行切割

foo|-|bar|-|zap

最终得到的结果是一组字符串,它们分别为foo、bar和zap。如果以-为切割符,则切割后的字符串数组包含:foo|、|bar|、|zap。我们看看SDS字符串库通过什么接口完成这个功能的

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);

第一个参数是需要被切割的字符串的指针,第二个参数是该字符串的长度。第三个参数是分隔符字符串的起始地址,第四个则是分隔符字符串的长度。通过这种形式传递进去的字符串,在其内容中是可以包含NULL的,因为提供了长度信息就意味着不用以NULL来查找字符串结尾了。该函数的返回值是一个SDS字符串数组的起始地址,这个数组的长度通过第五个参数返回。我们看下其实现:

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {int elements = 0, slots = 5, start = 0, j;sds *tokens;if (seplen < 1 || len < 0) return NULL;tokens = s_malloc(sizeof(sds)*slots);if (tokens == NULL) return NULL;if (len == 0) {*count = 0;return tokens;}

首先预分配了5个槽位用于存储切割留下的数据,然后遍历整个字符串空间,并使用分隔符进行对比,取出分割后的字符串

    for (j = 0; j < (len-(seplen-1)); j++) {/* make sure there is room for the next element and the final one */if (slots < elements+2) {sds *newtokens;slots *= 2;newtokens = s_realloc(tokens,sizeof(sds)*slots);if (newtokens == NULL) goto cleanup;tokens = newtokens;}/* search the separator */if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {tokens[elements] = sdsnewlen(s+start,j-start);if (tokens[elements] == NULL) goto cleanup;elements++;start = j+seplen;j = j+seplen-1; /* skip the separator */}}/* Add the final element. We are sure there is room in the tokens array. */tokens[elements] = sdsnewlen(s+start,len-start);

如果预分配的5个槽位不够,则在填充即将满了的时候,让槽位数量增加一倍。这些操作如果都成功,则返回SDS字符串数组,否则清空整个申请的空间

    if (tokens[elements] == NULL) goto cleanup;elements++;*count = elements;return tokens;cleanup:{int i;for (i = 0; i < elements; i++) sdsfree(tokens[i]);s_free(tokens);*count = 0;return NULL;}
}

在我们调用该方法获取到切割后的字符串数组后,我们要释放该数组的所有空间,以防止内存溢出问题。释放的方法是:

void sdsfreesplitres(sds *tokens, int count) {if (!tokens) return;while(count--)sdsfree(tokens[count]);s_free(tokens);
}

最后看下这些方法的使用样例:

sds *tokens;
int count, j;sds line = sdsnew("Hello World!");
tokens = sdssplitlen(line,sdslen(line)," ",1,&count);for (j = 0; j < count; j++)printf("%s\n", tokens[j]);
sdsfreesplitres(tokens,count);output> Hello
output> World!

命令行字符串拆解

之前介绍的字符串拆解方法要求入参字符串是严格按照一定方式布局的。然而用户输入形式的字符串,比如命令行,则非常可能不严格遵守格式。比如命令行中我们一般以空格分隔调用程序和其参数,但是并不严格要求使用几个空格去分隔

call "Sabrina"    and "Mark Smith\n"

上面这个命令行式的字符串则不能使用之前介绍的通过分隔符切割出各个字符串的方法,这时要准确切割需要使用:

sds *sdssplitargs(const char *line, int *argc)

这个方法第一个参数是待切割的字符串首地址,当然是以NULL结尾的。返回值是切割后的字符串数组首地址,第二个参数是用于传出这个数组的长度。它的实现就是从头向尾遍历整个字符串空间,然后切分出各个字符串。由于过程比较简单,但是代码比较长,我就不在这儿贴出来了。唯一要说的,可能引号匹配的场景截取稍微复杂点,因为引号里的空格是不能当成分隔符的。我们看下通过上面函数切分本节例子的结果

"call"
"Sabrina"
"and"
"Mark Smith\n"

字符串数组转字符串

有些场景下,我们会需要将“字符串切割成字符串数组”的行为逆过来,让字符串数组中对象逐个连接变成一个字符串。SDS字符串库提供了如下两个方法:

sds sdsjoin(char **argv, int argc, char *sep, size_t seplen);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);

sdsjoin方法针对C语言的字符串数组。第一个参数是字符串数组首地址。第二个参数是该数组的长度。第三个参数是连接各个字符串元素的“分隔符”字符串首地址。第四个参数是“分隔符”字符串的长度。sdsjoinsds方法则是针对sds字符串数组的。从这种参数设计可以看出,分隔符是可以包含NULL的,因为它提供了其长度信息。它们的实现也很简单:

sds sdsjoin(char **argv, int argc, char *sep) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscat(join, argv[j]);if (j != argc-1) join = sdscat(join,sep);}return join;
}sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscatsds(join, argv[j]);if (j != argc-1) join = sdscatlen(join,sep,seplen);}return join;
}

我们再看下其使用样例:

char *tokens[3] = {"foo","bar","zap"};
sds s = sdsjoin(tokens,3,"|",1);
printf("%s\n", s);output> foo|bar|zap

缩减字符串空间

SDS字符串在初次创建时,其分配空间大小就是使用了的空间大小。但是由于字符串连接等操作,会触发sdsMakeRoomFor方法,从而产生预分配的现象。这个时候往往被使用的空间大小只占已分配空间的一半。在大部分场景下,这种设计没有什么问题。但是对于内存特别紧张的时候,可能需要缩减这些字符串空间。SDS提供了如下方法实现空间缩减:

sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);type = sdsReqType(len);hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {newsh = s_malloc(hdrlen+len+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, len);return s;
}

这个操作一定会产生内存重分配的问题,所以它还是比较消耗效率的。好在需要它的场景不太多。

C语言字符串格式化SDS字符串

C语言中的字符串是以NULL结尾的,而SDS字符串可以包含NULL。如果我们希望SDS字符串按照C语言字符串格式一样,以NULL结尾,则可以调用如下方法:

void sdsupdatelen(sds s) {int reallen = strlen(s);sdssetlen(s, reallen);
}

这步操作非常简单,它只是以C语言字符串方式重新计算长度,并设置长度信息。它并没有进行字符串空间的重分配。我们看下例子:

sds s = sdsnew("foobar");
s[2] = '\0';
printf("%d\n", sdslen(s));
sdsupdatelen(s);
printf("%d\n", sdslen(s));output> 6
output> 2

基于上面的介绍,我们可以得知SDS字符串存在如下的特点:

  • 便于使用。我们可以像使用C语言中字符串一样接受和使用SDS字符串。比如我们可以
sds mystring = sdsnew("Hello World!");
mystring[0] = 'h';
printf("%s\n", mystring);
……
output>hello World!
  • 二进制安全。SDS字符串在头部使用len字段表示了buf中被使用了的空间长度,也就是说buf空间内容可以不用以NULL结尾。这种设计可以让SDS字符串承载包括NULL在内的一些二进制数据。
  • 执行高效。在字符串连接过程中,如果每次连接都要重新分配内存以承载更多的数据,会导致效率下降。而我们在SDS字符串头结构中看到有用于保存已分配空间的长度和已使用的空间的长度。

当然它也有相应的缺陷:

  • 可能要经常分配空间。一般一个字符串在第一次执行连接操作时,会发生原来字符串空间被释放,新空间被申请的过程。虽然作者做了优化,但是这个操作还是在所难免的。
  • 非线程安全的。我们在代码中没有看到任何线程安全性的辅助操作,所以它是线程非安全的。

本文的很多知识和样例来源于GitHub上的SDS说明:https://github.com/antirez/sds。我本来是想翻译这篇文章的,但是翻译过后感觉如果是内容直译可能不太符合大众的阅读口味,所以我就穿插的代码将它重新写了一遍。

Simple Dynamic Strings(SDS)源码解析和使用说明二相关推荐

  1. Simple Dynamic Strings(SDS)源码解析和使用说明一

    SDS是Redis源码中一个独立的字符串管理库.它是由Redis作者Antirez设计和维护的.一开始,SDS只是Antirez为日常开发而实现的一套字符串库,它被使用在Redis.Disque和Hi ...

  2. Redis源码-String:Redis String命令、Redis String存储原理、Redis String三种编码类型、Redis字符串SDS源码解析、Redis String应用场景

    Redis源码-String:Redis String命令.Redis String存储原理.Redis String三种编码类型.Redis字符串SDS源码解析.Redis String应用场景 R ...

  3. MyBatis源码解析(十二)——binding绑定模块之MapperRegisty

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6758456.html 1.回顾 之前解析了解析模块parsing,其实所谓的解析模块就是为 ...

  4. Myth源码解析系列之二-项目结构介绍

    上一篇我们了解了myth是什么及能做什么,下面我们来了解下项目主体结构吧 项目工程结构图 项目工程详解 myth-annotation myth分布式事务框架注解(如 @myth注解),业务层主要通过 ...

  5. @transactional注解原理_《Spring源码解析(十二)》深入理解Spring事务原理,告别面试一问三不知的尴尬...

    本文将带领大家领略Spring事务的风采,Spring事务是我们在日常开发中经常会遇到的,也是各种大小面试中的高频题,希望通过本文,能让大家对Spring事务有个深入的了解,无论开发还是面试,都不会让 ...

  6. Redis源码解析:21sentinel(二)定期发送消息、检测主观下线

    六:定时发送消息 哨兵每隔一段时间,会向其所监控的所有实例发送一些命令,用于获取这些实例的状态.这些命令包括:"PING"."INFO"和"PUBLI ...

  7. 激光SLAM源码解析S-LOAM(二)激光里程计的计算

    10Hz激光雷达点云帧,相临帧的时间间隔是0.1秒.在这0.1秒内激光雷达的位姿变化(平移和旋转)可由这两帧点云的配准计算出来. 以某一时刻为起点,累计帧间位姿变换,得到各帧时刻激光雷达相对起点的位姿 ...

  8. Android-PickerView系列之源码解析篇(二)

    前言 WheelView想必大家或多或少都有一定了解, 它是一款3D滚轮控件,效果类似IOS 上面的UIpickerview .按照国际惯例,先放一张效果图: 以上是Android-PickerVie ...

  9. 精尽 JDK 源码解析 —— 集合(二)链表 LinkedList

    1. 概述 LinkedList ,基于节点实现的双向链表的 List ,每个节点都指向前一个和后一个节点从而形成链表. 相比 ArrayList 来说,我们日常开发使用 LinkedList 相对比 ...

最新文章

  1. jar打包 jar line too long 异常处理方法
  2. 电商项目的并发量一般是多少_掌握这些,高并发秒杀系统就不用担心了!
  3. 【先生】丘成桐:中国人可以做世界一流学者
  4. 声音模仿_学唱歌 |唱歌声音难听的解决方法有哪些?这里给你总结总结!
  5. java培训每日总结,这是一份1000多字的Java培训总结,字字珠玑
  6. order by 影响效率么_内存碎片对性能的潜在影响
  7. java partialfunction,Java Relation.partialFunction方法代碼示例
  8. java把一个文件的内容复制到另外一个文件
  9. 工作流的大致开发流程
  10. c4d流体插件_C4D常用最火14款插件合集,总有一款你想要的!
  11. 如何用 NAS 搭建属于自己的云端书库 | 极客分享第 32 期
  12. 图片边缘出现黑点的问题分析和解决(纹理过滤)
  13. 网站CDN图片加速访问
  14. 审车的时候环保怎样安全通过_车审环保过不了如何处理
  15. 下载安装linux RedHat
  16. E. New Game Plus!(Technocup 2021 - Elimination Round 2)
  17. 研究生初试录取系统c++
  18. 91个排名:中国知名个人站长及发家史
  19. Python Level 4 程序题:商和余数,给定被除数和除数,求整数商及余数
  20. Could not identify launch activity: Default Activity not foundError while Launching activityFailed

热门文章

  1. 中国矿业大学计算机英语复试,中国矿业大学2020计算机科学与技术学院考研复试科目参考书目...
  2. IDEA Java解析GeoJson.json文件
  3. 使用Python、OpenCV进行图像接缝雕刻
  4. 使用Protobuf文件一键生成Java类
  5. LabVIEW做一款科学计算器
  6. 基于Struts2框架的名片管理系统
  7. opencv中LUT函数实现颜色空间缩减
  8. 【radar】毫米波雷达动态障碍物检测相关论文汇总(聚类、分类、稀疏2D点、4D点、雷达成像、原始数据处理)(4)
  9. Linux那些事儿 之 戏说USB(11)繁华落尽
  10. python3 ocr_OCR识别-python3.5版