2021SC@SDUSC

基于内容长度可变分块

滚动哈希与拉宾指纹

滚动哈希

考虑解决字符串匹配问题。一种方法是利用字符串哈希值进行匹配。已知模式串长度为nnn,那么我们可以依次截取匹配串中长为nnn的子串计算哈希,然后与模式串的哈希进行比对,若相等则得到一次匹配。在极大概率下,哈希碰撞到的子串与模式串相同。

然后用算法实现。在已经计算过si…i+n\small s_{i\dots i+n}si…i+n​哈希的前提下,如果要计算si+1…i+n+1\small s_{i+1\dots i+n+1}si+1…i+n+1​的哈希,若采用暴力方法,则需要扫描整个子串,其复杂度与暴力匹配相同,显然需要继续优化。

拉宾-卡普算法:https://zh.wikipedia.org/wiki/拉宾-卡普算法

考虑使用长为nnn的滑动窗口解决此问题:每扫过一个字符,从原哈希中删去旧字符的影响,然后加上新字符的影响,得到新哈希:

若采用此方法,则需要设计一个哈希函数支持动态地增删首尾字符的影响。可以考虑使用素域M\small MM上的多项式映射,其中n\small nn为窗口长度:(注意必须是素数,这样值域上各个值的概率才几乎相等)

hash(si…i+n−1)=(sian−1+si+1an−2+⋯+si+n−1a+si+n−1)(modM)hash(s_{i\dots i+n-1})=(s_{i}a^{n-1}+s_{i+1}a^{n-2}+\dots +s_{i+n-1}a+s_{i+n-1})_{\pmod{M}}hash(si…i+n−1​)=(si​an−1+si+1​an−2+⋯+si+n−1​a+si+n−1​)(modM)​

那么易推知:

hash(si+1…i+n)=(si+1an−1+si+2an−2+⋯+si+n−1a+si+n)(modM)hash(s_{i+1\dots i+n})=(s_{i+1}a^{n-1}+s_{i+2}a^{n-2}+\dots +s_{i+n-1}a+s_{i+n})_{\pmod{M}}hash(si+1…i+n​)=(si+1​an−1+si+2​an−2+⋯+si+n−1​a+si+n​)(modM)​

两式的递推关系如下:

hash(si+1…i+n)=(a⋅hash(si…i+n−1)(modM)−sian+si+n)(modM)hash(s_{i+1\dots i+n})=(a·hash(s_{i\dots i+n-1})_{\pmod{M}}-s_ia^{n}+s_{i+n})_{\pmod{M}}hash(si+1…i+n​)=(a⋅hash(si…i+n−1​)(modM)​−si​an+si+n​)(modM)​

因此就可以以O(1)\small O(1)O(1)的复杂度得到下一个窗口的哈希值。

# https://www.infoarena.ro/blog/rolling-hash
# a is a constant
an = 1 # a^n
rolling_hash = 0
for i in range(0, n):rolling_hash = (rolling_hash * a + S[i]) % MODan = (an * a) % MOD
if rolling_hash == hash_p:# match
for i in range(1, m - n + 1):rolling_hash = (rolling_hash * a + S[i + n - 1] - an * S[i - 1]) % MODif rolling_hash == hash_p:# match

其中an\small a^nan是常数,可以进行预处理。

拉宾指纹

拉宾指纹:https://zh.wikipedia.org/wiki/拉宾指纹

拉宾指纹也是一个多项式哈希映射,但它并非在素域M\small MM上,且映射结果不是一个值。拉宾指纹使用的是有限域GF(2)\small GF(2)GF(2)上的多项式,例如:f(x)=x3+x2+1f(x)=x^3+x^2+1f(x)=x3+x2+1。这个多项式可以使用二进制表示为1101\small 11011101。

之所以使用这样的多项式表示,是因为相比传统的值运算,GF(2)\small GF(2)GF(2)多项式运算更简单:加减都是异或,这样就完全不需要考虑进位的问题。并且多项式的乘除性质与整数相似。不过就算不需要考虑进位,乘法和除法(求余)也只能以k\small kk的复杂度完成(k\small kk为多项式的最高次幂)。

拉宾指纹的哈希函数如下:(和素域类似,模需要是个不可约多项式)

hash(si…i+n)=(sia(x)n+si+1a(x)n−1+⋯+si+n−1a(x)+si+n)(modM(x))hash(s_{i\dots i+n})=(s_{i}a(x)^n+s_{i+1}a(x)^{n-1}+\dots +s_{i+n-1}a(x)+s_{i+n})_{\pmod{M(x)}}hash(si…i+n​)=(si​a(x)n+si+1​a(x)n−1+⋯+si+n−1​a(x)+si+n​)(modM(x))​

递推式如下:

hash(si+1…i+n)=((a(x)⋅hash(si…i+n−1))(modM(x))−sian(x)+si+n)(modM(x))hash(s_{i+1\dots i+n})=((a(x)·hash(s_{i\dots i+n-1}))_{\pmod{M(x)}}-s_ia^{n}(x)+s_{i+n})_{\pmod{M(x)}}hash(si+1…i+n​)=((a(x)⋅hash(si…i+n−1​))(modM(x))​−si​an(x)+si+n​)(modM(x))​

实现

首先选择有限域GF(2)\small GF(2)GF(2)多项式的模p(x)p(x)p(x),要求是个不可约多项式。这里选取了一个k=64\small k=64k=64的多项式。

static u_int64_t poly = 0xbfe6b8a5bf378d83LL;

考虑滑动窗口每次滑动一个字节,则可以令a(x)=x8a(x)=x^8a(x)=x8。

根据拉宾指纹递推式,若直接运算,则需要进行2\small 22次乘法和1\small 11次求余,其复杂度为O(k)\small O(k)O(k)。虽说k\small kk是个常数,但如果优化了kkk,再结合GF(2)\small GF(2)GF(2)多项式无需进位的性质,拉宾指纹的性能甚至会超过传统哈希。

将递推式分为三个部分:不可预处理的乘法部分hash(si…i+n)⋅a(x)\small hash(s_{i\dots i+n})·a(x)hash(si…i+n​)⋅a(x)、可预处理的乘法部分sian+1s_ia^{n+1}si​an+1、加法部分si+n+1\small s_{i+n+1}si+n+1​。第一个部分不可预处理,因为哈希值是不可预知的;第二个部分可预处理,因为sis_isi​作为一个字节只有28\small 2^828种取值,且an(x)a^n(x)an(x)也是一个常量;最后一个部分只需一个异或运算,可忽略。

首先考虑第二部分。先实现多项式乘除法,然后易求得an(x)a^n(x)an(x),接着枚举sis_isi​的值并求得sian(x)s_ia^n(x)si​an(x),最后将结果缓存到U\small UU表中。对幂运算部分继续优化:假如已知处理第一部分的算法为MUL(p, a),其复杂度为O(1)\small O(1)O(1),那么幂运算就等价于p=MUL(p, a)进行nnn次,这样就优化掉了常数kkk。

然后考虑优化第一部分,我们需要计算的是(p(x)⋅a(x))(modM(x))(\small p(x)·a(x))_{\pmod{M(x)}}(p(x)⋅a(x))(modM(x))​。优化过程如下:

  1. 对乘法优化

    发现每次都是乘以a(x)=x8\small a(x)=x^8a(x)=x8,若以二进制表示,就是左移八位。左移复杂度为O(1)\small O(1)O(1)。

  2. 对求余优化

    令g(x)\small g(x)g(x)等于p(x)\small p(x)p(x)的最高次项xshiftxx^{shiftx}xshiftx,则必有g(x)≤p(x)\small g(x)\le p(x)g(x)≤p(x)。原式可改写为:

    ((p−p(modg/a)+p(modg/a))⋅a)(modM)((p-p_{\pmod{g/a}}+p_{\pmod{g/a}})·a)_{\pmod{M}}((p−p(modg/a)​+p(modg/a)​)⋅a)(modM)​

    对其进行变换:

    =(p(modg/a)⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)=((p−(p−p(modg/a)))⋅a)(modg/a)+((p−p(modg/a))⋅a)(modM)\begin{array}{ll}=(p_{\pmod{g/a}}·a)_{\pmod{g/a}}+((p-p_{\pmod{g/a}})·a)_{\pmod{M}}\\=((p-(p-p_{\pmod{g/a}}))·a)_{\pmod{g/a}}+((p-p_{\pmod{g/a}})·a)_{\pmod{M}}\end{array}=(p(modg/a)​⋅a)(modg/a)​+((p−p(modg/a)​)⋅a)(modM)​=((p−(p−p(modg/a)​))⋅a)(modg/a)​+((p−p(modg/a)​)⋅a)(modM)​​

    注意到第一个子式一定小于ggg,因此一定小于aaa。令j⋅(ga)=p−p(modg/a)\small j·(\frac{g}{a})=p-p_{\pmod{g/a}}j⋅(ag​)=p−p(modg/a)​,则上式可改写为:

    ((p−j⋅(ga))⋅a)+(g⋅j)(modM)((p-j·(\frac{g}{a}))·a)+(g·j)_{\pmod{M}}((p−j⋅(ag​))⋅a)+(g⋅j)(modM)​

    其中g/a=xshiftx−8\small g/a=x^{shiftx-8}g/a=xshiftx−8,因此p−p(modg/a)p-p_{\pmod{g/a}}p−p(modg/a)​就是保留ppp的高八位其余位填零;而jjj就是ppp的高八位。将各个式子改写为二进制形式,并使用位运算,则上式等价于:

    (p^j)<<8+g*j%(xshift-8)
    

继续改写,将ppp提出来:

(p<<8) ^ (g*j%(xshift-8)|j<<(xshift+8))

所以预处理g*j%(xshift-8)|j<<(xshift+8),就可以以O(1)\small O(1)O(1)计算第一部分。将前式缓存至T\small TT,并将其与第三部分结合,得到最终代码:

// (p * a + m) % M
(p<<8|m)^T[p>>xshift-8]
  • 相关源码

    int xshift, shift
    static inline char fls64(u_int64_t v); // 获取最高位的1在哪一位
    u_int64_t polymod(u_int64_t nh, u_int64_t nl, u_int64_t d); // (nh<<64|nl) % d
    void polymult(u_int64_t *php, u_int64_t *plp, u_int64_t x, u_int64_t y); // x * y = (php<<64|plp)
    static u_int64_t append8(u_int64_t p, u_char m) {return ((p << 8) | m) ^ T[p >> shift];
    }
    static void calcT (u_int64_t poly) { // Tint j = 0;xshift = fls64(poly) - 1;shift = xshift - 8;u_int64_t T1 = polymod(0, INT64 (1) << xshift, poly);for (j = 0; j < 256; j++) {T[j] = polymmult(j, T1, poly) | ((u_int64_t) j << xshift);}
    }
    static void calcU(int size) // U
    {int i;u_int64_t sizeshift = 1;for (i = 1; i < size; i++)sizeshift = append8(sizeshift, 0);for (i = 0; i < 256; i++)U[i] = polymmult(i, sizeshift, poly);
    }
    void rabin_init(int len) { // 初始化calcT(poly);calcU(len);
    }
    unsigned int rabin_checksum(char *buf, int len) { // 首次计算,窗口长度为lenint i;unsigned int sum = 0;for (i = 0; i < len; ++i) {sum = rabin_rolling_checksum (sum, len, 0, buf[i]);}return sum;
    }
    unsigned int rabin_rolling_checksum(unsigned int csum, int len,char c1, char c2) { // 滚动计算return append8(csum ^ U[(unsigned char)c1], c2); // (csum*a+c2-a^len*c1)%poly
    }
    

    在git中也用到了此技术,源码位于:git/diff-delta,可作参考学习

CDC

问题的引入

  1. 为什么要分块

    对于本地文件系统,分块用于解决孔问题、方便操作系统管理空间、降低扫描次数等。而对于网络文件系统更是如此,分块后只需同步那些被修改的块,比重新上传整个文件更有效率。

  2. 定长分块

    现在对文件进行定长分块,假设文件中的内容为abcdefg,每四个字节分为一块,则分块后为abcd|efg。假如在头部加入了一个字符,内容变更为0abcdefg,则分块后为0abc|defg,发现两个块和之前完全不一样。这意味着如果要向网络文件系统同步此次修改,则需重新上传两个块。

  3. 基于内容可变长度的分块

    假如我们基于内容进行分块,以d作为断点,在d后产生断点,那么此时分块就变成了0abcd|efg。发现这样分块与之前的分块仅一个块不一致,也就是说只需重新上传这个不一致的块,相比定长分块效率大大提高。

  4. 缺点

    有极低的概率出现多个短块。如ddddd断开,则会得到d|d|d|d。此情况会导致块数过多,因而变得难以维护。

滚动哈希与断点

显然不能总是以相同的内容作为断点,例如若文件内容为dd...d,则分块后为d|d|...|d|。每个块仅包含一个字符,非常浪费空间,更不方便管理,违背了分块的初衷。

我们希望以某种概率分布选择断点使得各个块有一个平均大小,同时又保证断点的某种属性相同。发现哈希恰好能满足这两个性质。

哈希是伪随机的,对于b\small bb位的二进制哈希值,其出现的概率都是2−b2^{-b}2−b。这时候考虑此前的滚动哈希,假设滑动窗口每滑动一次产生的哈希都是随机的,那么上一次碰撞到下一次碰撞的长度即块长恰好呈几何分布:

pdf(l)=(1−2−b)l−1⋅2−b\mathit{pdf}(l)=(1-2^{-b})^{l-1}·2^{-b}pdf(l)=(1−2−b)l−1⋅2−b

那么期望的块长就是2b2^b2b。

上下限

虽然哈希使得块长有预期的平均,但这是基于随机的数据,仍然存在特殊的数据使得特殊的情况出现,而一旦这种情况出现,将大大降低分块的效率。因此采用了一个折中的方案,限制块长的最小值与最大值。当以最小块长或最大块长分块时(非变长分块),块的性质类似于定长分块。

若平均块长为2202^{20}220字节(1MB),最小块长为2182^{18}218字节(256KB),最大块长为2222^{22}222字节(4MB),可以通过几何分布计算非变长分块出现的概率:

P(l≤218)+P(l≥222)=∑i=1218pdf(i)+∑i=222∞pdf(i)=1−∑i=218222pdf(i)≈0.239515P(l\le 2^{18})+P(l\ge 2^{22})=\sum_{i=1}^{2^{18}}\mathit{pdf}(i)+\sum_{i=2^{22}}^{\infty}\mathit{pdf}(i)=1-\sum_{i=2^{18}}^{2^{22}}\mathit{pdf}(i)\approx 0.239515P(l≤218)+P(l≥222)=i=1∑218​pdf(i)+i=222∑∞​pdf(i)=1−i=218∑222​pdf(i)≈0.239515

也就是说断点的命中率约为p′=76%p'=\small 76\%p′=76%。假设数据中一个子序列sss在修改前后不变,其中包含了kkk个断点,那么可以通过几何分布算出CDC在第iii个断点首次命中的概率为:

pdf(i)=p′(1−p′)i−1\mathit{pdf}(i)=p'(1-p')^{i-1}pdf(i)=p′(1−p′)i−1

因此期望在第1p′≈1.314950\frac{1}{p'}\approx 1.314950p′1​≈1.314950个断点命中。由于断点间隔的期望为2202^{20}220,那么对应的,未命中的数据长度期望则是2bp′≈1.3\frac{2^b}{p'}\approx 1.3p′2b​≈1.3MB。一旦命中断点,后面的分块情况完全相同。也就是说在这个子序列中平均只有约1.3MB的数据被重新以最大长度或最小长度分块了,其余的块仍然不变。

(实际项目中定义平均块长为8MB,最小块长为6MB,最大块长为10MB。由此求得命中率约为0.185862\small 0.1858620.185862;其中块长为6MB的概率约为0.527633\small 0.5276330.527633,为10MB的概率约为0.286505\small 0.2865050.286505。虽然命中率低,但重分块的期望也仅2bp′=80.185862≈43\frac{2^b}{p'}=\frac{8}{0.185862}\approx 43p′2b​=0.1858628​≈43MB,这对于G甚至T级别的文件来讲微不足道)

实现

/*最小块长略与文件缓冲区略,主要看以下分块部分
*/
if (cur < block_min_sz - 1) // 最小块长cur = block_min_sz - 1;
while (cur < tail) { // 一直扫描直到达到tailfingerprint = (cur == block_min_sz - 1) ?  // 计算指纹finger(buf + cur - BLOCK_WIN_SZ + 1, BLOCK_WIN_SZ) : // 首次计算rolling_finger (fingerprint, BLOCK_WIN_SZ, *(buf+cur-BLOCK_WIN_SZ), *(buf + cur)); // 滚动计算if (((fingerprint & block_mask) ==  ((BREAK_VALUE & block_mask)))|| cur + 1 >= file_descr->block_max_sz) // 碰撞,找到断点{if (file_descr->block_nr == file_descr->max_block_nr) {seaf_warning ("Block id array is not large enough, bail out.\n");free (buf);return -1;}gint64 idx_size = cur + 1;WRITE_CDC_BLOCK (cur + 1, write_data); // 将buf[0..cur]写入块if (indexed) // 记录已处理的长度*indexed += idx_size;break;} else { // 继续寻找cur ++;}
}

相关函数释义

具体注释详见:https://github.com/poi0qwe/seafile-server-learn/tree/main/common/cdc

  • 将块写入文件 / WriteblockFunc

    static int default_write_chunk (CDCDescriptor *chunk_descr) // 默认写块文件的方法
    {char filename[NAME_MAX_SZ];char chksum_str[CHECKSUM_LENGTH *2 + 1];int fd_chunk, ret;memset(chksum_str, 0, sizeof(chksum_str));rawdata_to_hex (chunk_descr->checksum, chksum_str, CHECKSUM_LENGTH); // 将checksum转为HEX串snprintf (filename, NAME_MAX_SZ, "./%s", chksum_str); // 设置文件名为checksum的HEX串,并限定其最大长度为`NAME_MAX_SZ-1`fd_chunk = g_open (filename, O_RDWR | O_CREAT | O_BINARY, 0644); // 创建文件,并写文件if (fd_chunk < 0) // 打开文件失败return -1;    ret = writen (fd_chunk, chunk_descr->block_buf, chunk_descr->len); // 将缓冲写入文件,写n个close (fd_chunk); // 关闭文件return ret; // 返回写的结果
    }
    
  • 初始化

    // 给定文件,初始化它的文件分块信息(只用到了文件大小)
    static int init_cdc_file_descriptor (int fd,uint64_t file_size,CDCFileDescriptor *file_descr)
    {int max_block_nr = 0;int block_min_sz = 0;file_descr->block_nr = 0; // 实际块数// 若为空,则设置默认值if (file_descr->block_min_sz <= 0)file_descr->block_min_sz = BLOCK_MIN_SZ;if (file_descr->block_max_sz <= 0)file_descr->block_max_sz = BLOCK_MAX_SZ;if (file_descr->block_sz <= 0)file_descr->block_sz = BLOCK_SZ;if (file_descr->write_block == NULL)file_descr->write_block = (WriteblockFunc)default_write_chunk; // 默认写块文件的方法block_min_sz = file_descr->block_min_sz; // 块的最小大小max_block_nr = ((file_size + block_min_sz - 1) / block_min_sz); // 计算最大块数(极值)file_descr->blk_sha1s = (uint8_t *)calloc (sizeof(uint8_t),max_block_nr * CHECKSUM_LENGTH); // 按照最大块数申请空间file_descr->max_block_nr = max_block_nr;return 0;
    }
    
  • 写数据与校验和

    // 写一个块(block_sz是块的大小,write_data表示是否写入硬盘)
    #define WRITE_CDC_BLOCK(block_sz, write_data)                \
    do {                                                         \int _block_sz = (block_sz);                              \chunk_descr.len = _block_sz;                             \ // 设置缓冲长度chunk_descr.offset = offset;                             \ // 设置偏移ret = file_descr->write_block (file_descr->repo_id,      \ // 写文件方法file_descr->version,         \&chunk_descr,                \crypt,                       \chunk_descr.checksum,        \(write_data));               \if (ret < 0) {                                           \ // 写失败free (buf);                                          \g_warning ("CDC: failed to write chunk.\n");         \return -1;                                           \}                                                        \memcpy (file_descr->blk_sha1s +                          \file_descr->block_nr * CHECKSUM_LENGTH,          \chunk_descr.checksum, CHECKSUM_LENGTH);          \ // 记录块的SHA1SHA1_Update (&file_ctx, chunk_descr.checksum, 20);       \ // 更新SHA1file_descr->block_nr++;                                  \ // 记录块数offset += _block_sz;                                     \ // 记录偏移\memmove (buf, buf + _block_sz, tail - _block_sz);        \ // 更新buftail = tail - _block_sz;                                 \cur = 0;                                                 \ // 移动指针
    }while(0); // 表示执行一次
    
  • 分块主函数

    int file_chunk_cdc(int fd_src, // 文件标识符CDCFileDescriptor *file_descr, // 文件分块信息,新建或更新SeafileCrypt *crypt, // 加密信息gboolean write_data, // 是否写入硬盘gint64 *indexed) // 块索引
    // 注释详见:https://github.com/poi0qwe/seafile-server-learn/blob/main/common/cdc/cdc.c
    

【山大智云】SeafileServer源码分析之CDC(基于内容长度可变分块)相关推荐

  1. 【山大智云开发日志】项目安装与部署

    2021SC@SDUSC 目录 简介 操作系统 前置操作 安装中文语言 安装前置组件 准备数据库 建表 创建master用户 下载项目源代码 编译并安装项目 创建配置文件 seafile-server ...

  2. 阿里云image-syncer源码分析

    阿里云image-syncer源码分析 欢迎关注"云原生手记"微信公众号 背景 大家在公司中都会使用到容器镜像私有仓库,一般都用harbor,也有会用registry搭建一个简陋的 ...

  3. 大数据之Oozie——源码分析(一)程序入口

    工作中发现在oozie中使用sqoop与在shell中直接调度sqoop性能上有很大的差异.为了更深入的探索其中的缘由,开始了oozie的源码分析之路.今天第一天阅读源码,由于没有编译成功,不能运行测 ...

  4. TreeMap源码分析——深入分析(基于JDK1.6)

    TreeMap有Values.EntrySet.KeySet.PrivateEntryIterator.EntryIterator.ValueIterator.KeyIterator.Descendi ...

  5. OkHttpClient 源码分析 1(基于3.9.0的源码)

    OkHttpClient是目前开发 android 应用使用最广泛的网络框架,最近看了阿里的 httpdns 里面对于 dns 的处理,我们团队就想调研一下在项目中有什么利弊,并且框架中是否对 soc ...

  6. Python wordcloud词云:源码分析及简单使用

    Python版本的词云生成模块从2015年的v1.0到现在,已经更新到了v1.7. 下载请移步至:https://pypi.org/project/wordcloud/ wordcloud简单应用: ...

  7. 从源码分析常见的基于Array的数据结构动态扩容机制

    本文的写作冲动来源于今晚看到的老赵的一则微博"大家知道System.Collections.Generic.List<T>是一种什么样的数据结构?内部的元素是怎么存放的?还有Di ...

  8. ZedGraph5.1.5源码分析去掉鼠标悬浮内容闪烁问题(附源码下载)

    场景 在使用ZedGraph绘制曲线图时,将鼠标悬浮时内容闪烁,且频率很高. 找到其源码,发现不论鼠标移动的范围大小,甚至乎不论鼠标是否移动,都要刷新一次Tooltip. 注: 博客主页: https ...

  9. Struts 源码分析笔记1(尚无内容-请跳过,省得浪费时间)

    只是单纯的 学习笔记..可能会毫无章法.目前对Struts的整个架构设计尚不不清晰 有错误敬请指出,谢谢.今天只开个头.. 随便写个大概的计划: 先把Struts框架所提供的基本功能 ,如:处理请求过 ...

  10. docker 源码分析 三(基于1.8.2版本),NewDaemon启动

    本文来分析一下New Daemon的启动过程:在daemon/daemon.go文件中: func NewDaemon(config *Config, registryService *registr ...

最新文章

  1. shell (check return of each line)(PIPESTATUS[@])and sudoer
  2. 学习笔记之12个月提升计划
  3. 一入前端深似海,从此红尘是路人系列第七弹之孤独的剑客-单例模式
  4. 小米11 Pro概念图曝光:曲面挖孔屏+后置五摄相机模组
  5. angular监听输入框值的变化_angular 实时监听input框value值的变化触发函数方法
  6. [已解决]Tomcat启动报 java.net.BindException: Address already in use: JVM_Bind
  7. 券商IT的建设一定要有全局观、前瞻性,要走在业务前面,而不是被动响应
  8. GY-53红外激光测距模块的使用以及pwm模式代码的实现
  9. 免费、正版、最新的Idea(教育免费版)获取流程!!
  10. Hibernate表间映射时HHH000142异常
  11. 深度学习入门笔记(李沐)(一)
  12. Python 3,一行代码处理各种时间转换,从此跟datetime,time模块说拜拜 ~ ~ 不收藏算我输!!!
  13. Tomcat多实例与负载均衡
  14. Springer投稿流程——Multimedia Tools and Applications
  15. c语言分支编程改错题,二级C语言改错 二级C语言编程题 汇总整理篇.doc
  16. 制作 macOS U盘USB启动安装盘方法
  17. python调用ansys
  18. mysql 关联查询连接条件
  19. “世上唯一的后悔药”:冻卵?
  20. 计算机信息技术导论知识点,《信息技术导论》复习资料(ldst).doc

热门文章

  1. 同一包(package)下,两个不同类的调用操作详解
  2. css3复习知识点概括1(根据W3S顺序)
  3. 百思不得其姐学习笔记
  4. 爬取百思不得姐段子图片
  5. Blender_1_移动、旋转、缩放
  6. 《大数据之路:阿里巴巴大数据实践》第一篇 数据技术篇-读书笔记
  7. 计算机需要权限来执行此操作 win7,Win7系统下“文件夹访问被拒绝 您需要权限来执行操作”解决方法...
  8. Java获取今天是星期几
  9. java calendar星期_作业-用Calendar获取今天是星期几
  10. photoshop-photoshop记录