Redis中的列表对象比较特殊,在版本3.2之前,列表底层的编码是 ziplist 和 linkedlist 实现的, 但是在版本3.2之后,重新引入了一个 quicklist 的数据结构,列表的底层都由quicklist实现。
这边是在看源码和实际验证的时候发现的区别,然后上网查证。由于目前使用的redis基本都在3.2了, 而且老版本肯定会被取代, 所以我们只分析3.2版本之后的实现。对于老版本的列表实现,一笔带过吧。
在老版本中,当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:
列表对象保存的所有字符串元素的长度都小于 64 字节;
列表对象保存的元素数量小于 512 个;
不能满足这两个条件的列表对象需要使用 linkedlist 编码。当这两个条件任何一个不满足的时候,就会有一个格式的转换。
对于quicklist的结构,下节中在具体分析,先来看下list的实现。
List的结构
/* Structure to hold list iteration abstraction. */
typedef struct {
robj *subject;
unsigned char encoding;
unsigned char direction; /* Iteration direction */
quicklistIter *iter;
} listTypeIterator;
/* Structure for an entry while iterating over a list. */
typedef struct {
listTypeIterator *li;
quicklistEntry entry; /* Entry in quicklist */
} listTypeEntry;
1
2
3
4
5
6
7
8
9
10
11
12
13
List的结构其实就是定义了一个列表的头节点, 以及一个迭代器指针,指针中指定了编码格式和迭代方向。
List命令
命令 说明
BLPOP key1 [key2 ] timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOPLPUSH source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它;如但果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
LINDEX key index 通过索引获取列表中的元素
LINSERT key BEFORE AFTER pivot value
LLEN key 获取列表长度
LPOP key 移出并获取列表的第一个元素
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LPUSHX key value 将一个或多个值插入到已存在的列表头部
LRANGE key start stop 获取列表指定范围内的元素
LREM key count value 移除列表元素
LSET key index value 通过索引设置列表元素的值
LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
RPOP key 移除并获取列表最后一个元素
RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
RPUSH key value1 [value2] 在列表中添加一个或多个值
RPUSHX key value 为已存在的列表添加值
List命令实现
push命令实现
lpush和rpush分别调用的下边的两个函数:
// lpush操作
void lpushCommand(client *c) {
pushGenericCommand(c,LIST_HEAD);
}
// rpush操作
void rpushCommand(client *c) {
pushGenericCommand(c,LIST_TAIL);
}
1
2
3
4
5
6
7
8
9
可以看到, 这两个操作其实都是调用的pushGenericCommand这个函数实现, 不同的点是指定了是从HEAD的位置push一个数据还是从TAIL的位置push一个数据。接下来来看pushGenericCommand的实现。
void pushGenericCommand(client *c, int where) {
int j, pushed = 0;
// 现在数据库中查找是否已经存在了该键
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
// 如果已经存在了该键,验证该键是否是list类型,如果不是的话,返回错误
if (lobj && lobj->type != OBJ_LIST) {
addReply(c,shared.wrongtypeerr);
return;
}
// 遍历剩余的参数
for (j = 2; j < c->argc; j++) {
// 如果该键不存在,创建一个quicklist
if (!lobj) {
lobj = createQuicklistObject();
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
// 将创建的键添加到对应的db
dbAdd(c->db,c->argv[1],lobj);
}
// 执行push操作
listTypePush(lobj,c->argv[j],where);
// 个数加1
pushed++;
}
// 返回添加的节点数量
addReplyLongLong(c, (lobj ? listTypeLength(lobj) : 0));
// 至少有一个添加成功则进行操作
if (pushed) {
char *event = (where == LIST_HEAD) ? "lpush" : "rpush";
// 发送键修改信号
signalModifiedKey(c->db,c->argv[1]);
// 发送事件通知
notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
}
// 服务器的脏数据个数增加
server.dirty += pushed;
}
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
内部最终还是调用了一个listTypePush的函数:
/* The function pushes an element to the specified list object 'subject',
* at head or tail position as specified by 'where'.
*
* There is no need for the caller to increment the refcount of 'value' as
* the function takes care of it if needed. */
void listTypePush(robj *subject, robj *value, int where) {
// 判断类型是否为quciklist,不是的话返回错误
if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
// 解码数据
value = getDecodedObject(value);
// 得到数据的长度
size_t len = sdslen(value->ptr);
// 调用quicklistPush插入数据
quicklistPush(subject->ptr, value->ptr, len, pos);
// 将数据项对象的引用次数减1,也就是释放value
decrRefCount(value);
} else {
serverPanic("Unknown list encoding");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pop的操作其实跟push的操作很相似,比较容易读懂。List的操作比较有特点的一项是阻塞操作,可以来分析一下。
阻塞pop操作的实现
首先,一样是调用了最上层的两个函数:
// blpop
void blpopCommand(client *c) {
blockingPopGenericCommand(c,LIST_HEAD);
}
// brpop
void brpopCommand(client *c) {
blockingPopGenericCommand(c,LIST_TAIL);
}
1
2
3
4
5
6
7
8
他们其实都是调用blockingPopGenericCommand来实现的。
/* Blocking RPOP/LPOP */
void blockingPopGenericCommand(client *c, int where) {
robj *o;
mstime_t timeout;
int j;
// 取出timeout参数
if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
!= C_OK) return;
// 遍历参数
for (j = 1; j < c->argc-1; j++) {
// 查看数据库中是否存在该键
o = lookupKeyWrite(c->db,c->argv[j]);
if (o != NULL) {
// 如果存在,判断该键是否是list类型,不是的话报错
if (o->type != OBJ_LIST) {
addReply(c,shared.wrongtypeerr);
return;
} else {
// 当前列表非空,直接执行pop操作
if (listTypeLength(o) != 0) {
/* Non empty list, this is like a non normal [LR]POP. */
char *event = (where == LIST_HEAD) ? "lpop" : "rpop";
robj *value = listTypePop(o,where);
serverAssert(value != NULL);
addReplyMultiBulkLen(c,2);
addReplyBulk(c,c->argv[j]);
addReplyBulk(c,value);
decrRefCount(value);
notifyKeyspaceEvent(NOTIFY_LIST,event,
c->argv[j],c->db->id);
// 如果当前key弹出一个值之后为空,删除这个列表
if (listTypeLength(o) == 0) {
dbDelete(c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",
c->argv[j],c->db->id);
}
signalModifiedKey(c->db,c->argv[j]);
server.dirty++;
/* Replicate it as an [LR]POP instead of B[LR]POP. */
rewriteClientCommandVector(c,2,
(where == LIST_HEAD) ? shared.lpop : shared.rpop,
c->argv[j]);
return;
}
}
}
}
/* If we are inside a MULTI/EXEC and the list is empty the only thing
* we can do is treating it as a timeout (even with timeout 0). */
if (c->flags & CLIENT_MULTI) {
addReply(c,shared.nullmultibulk);
return;
}
/* If the list is empty or the key does not exists we must block */
// 参数中的所有键都不存在,则阻塞这些键
blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}
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
可以看到, 当指定的list存在于当前数据库中且list不为空,就会执行一次普通的pop操作;但是当指定的list键不存在,或者该list为空,就会阻塞该操作。就是上边代码中的最后一句。
接下去,就是看redis如何处理这个被阻塞的操作。
/* Set a client in blocking mode for the specified key, with the specified
* timeout */
// 设置键的阻塞状态
void blockForKeys(client *c, robj **keys, int numkeys, mstime_t timeout, robj *target) {
dictEntry *de;
list *l;
int j;
c->bpop.timeout = timeout;
c->bpop.target = target;
if (target != NULL) incrRefCount(target);
// 遍历所有的key
for (j = 0; j < numkeys; j++) {
/* If the key already exists in the dict ignore it. */
// 如果当前键存在,则忽略;反之则添加该键
// bpop.keys记录所有造成客户端阻塞的键
if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
//当前的key引用计数加1
incrRefCount(keys[j]);
/* And in the other "side", to map keys -> clients */
// blocking_keys是一个字典,其键为造成阻塞的键,值是一个链表,记录所有被该键阻塞的客户端
// 查找当前造成阻塞的键
de = dictFind(c->db->blocking_keys,keys[j]);
if (de == NULL) {
// 如果不存在,需要新创建一个, 并加入到blocking_keys中
int retval;
/* For every key we take a list of clients blocked for it */
l = listCreate();
// 将键和新创建的列表加入
retval = dictAdd(c->db->blocking_keys,keys[j],l);
incrRefCount(keys[j]);
serverAssertWithInfo(c,keys[j],retval == DICT_OK);
} else {
// 如果存在,获取该键的值,即客户端列表
l = dictGetVal(de);
}
// 将当前的客户端加入的该键的阻塞列表中
listAddNodeTail(l,c);
}
// 阻塞该客户端
blockClient(c,BLOCKED_LIST);
}
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
从上边的代码中,可以看到客户端分别用来c->bpop.xxxx 和 c->db->blocking_keys用来保存被阻塞的键,以及阻塞的键和客户端的对应关系。
他们的定义如下:
// server.h
typedef struct client {
//client当前使用的数据库
redisDb *db; /* Pointer to currently SELECTed DB. */
//阻塞状态
blockingState bpop; /* blocking state */
//其他成员省略
} client;
// 阻塞状态结构体
typedef struct blockingState {
mstime_t timeout; // 阻塞超时时间
dict *keys; // 记录所有造成客户端阻塞的键
robj *target; // 目标选项,target在执行RPOPLPUSH命令时使用,
/* BLOCKED_WAIT */
int numreplicas; /* Number of replicas we are waiting for ACK. */
long long reploffset; /* Replication offset to reach. */
} blockingState;
typedef struct redisDb {
dict *blocking_keys; // 记录所有造成阻塞的键,及其相应的客户端
// ...其他参数省略
} redisDb;
typedef struct redisDb {
//正处于阻塞状态的键
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
//可以解除阻塞的键
dict *ready_keys; /* Blocked keys that received a PUSH */
} redisDb;
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
redisDb里边利用了一个名为blocking_keys的dict来存储每个阻塞的键,以及等待该键的客户端的对应关系。
做完这些之后,这个客户端就被阻塞了。 那么这个客户端如何从阻塞状态重新回复到非阻塞状态呢。 一个当时是等待的时间超过了timeout的时间,从阻塞状态恢复; 另一个则是其他的客户端往这个列表中插入了数据,正好是当前阻塞的客户端所需要的,当前客户端收到信号之后,从阻塞状态中恢复。
根据我的理解, 如果是客户端超时被解阻塞,调用的是这个函数:
超时解阻塞
//解阻塞一个正在阻塞中的client
void unblockClientWaitingData(client *c) {
dictEntry *de;
dictIterator *di;
list *l;
serverAssertWithInfo(c,NULL,dictSize(c->bpop.keys) != 0);
//创建一个字典的迭代器,指向的是造成client阻塞的键所组成的字典
di = dictGetIterator(c->bpop.keys);
/* The client may wait for multiple keys, so unblock it for every key. */
//因为client可能被多个key所阻塞,所以要遍历所有的键
while((de = dictNext(di)) != NULL) {
robj *key = dictGetKey(de); //获得key对象
/* Remove this client from the list of clients waiting for this key. */
//根据key找到对应的列表类型值,值保存着被阻塞的client,从中找c->db->blocking_keys中寻找
l = dictFetchValue(c->db->blocking_keys,key);
serverAssertWithInfo(c,key,l != NULL);
// 将阻塞的client从列表中移除
listDelNode(l,listSearchKey(l,c));
/* If the list is empty we need to remove it to avoid wasting memory */
//如果当前列表为空了,则从c->db->blocking_keys中将key删除
if (listLength(l) == 0)
dictDelete(c->db->blocking_keys,key);
}
dictReleaseIterator(di); //释放迭代器
/* Cleanup the client structure */
//清空bpop.keys的所有节点
dictEmpty(c->bpop.keys,NULL);
//如果保存有新添加的元素,则应该释放
if (c->bpop.target) {
decrRefCount(c->bpop.target);
c->bpop.target = NULL;
}
}
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
如果是其他的客户端插入了一个数据,则是调用下边的函数:
插入解阻塞
// 如果客户端因为等待某个 key 被阻塞,那么将此key加入到server.ready_keys中
// 这个列表最终会被 handleClientsBlockedOnLists() 函数处理。
void signalListAsReady(redisDb *db, robj *key) {
readyList *rl;
// 如果在所有造成客户端阻塞的键中找不到此键,则不作处理
if (dictFind(db->blocking_keys,key) == NULL) return;
// 这个键已经存在于ready_keys中了,则不作处理
if (dictFind(db->ready_keys,key) != NULL) return;
// 创建一个新的readylists结构,保存键和数据库
// 然后将该结构添加到server.ready_keys中
rl = zmalloc(sizeof(*rl));
rl->key = key;
rl->db = db;
// 该键的索引加1
incrRefCount(key);
listAddNodeTail(server.ready_keys,rl);
// 同样,将key添加到db->ready_keys中
incrRefCount(key);
serverAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在将所有可用的key加入到ready_keys之后,会有统一的函数去检查哪些客户端等待着这些key,然后将他们解阻塞
/* 遍历server.ready_keys中所有已经准备好的key,同时在c->db->blocking_keys中
遍历所有由此键造成阻塞的客户端,如果key不为空的话,就从key中弹出一个元素返回给客户端并解除该客户端的阻塞状态,直到server.ready_keys为空,或没有因该key而阻塞的客户端为止 */
/* This function should be called by Redis every time a single command,
* a MULTI/EXEC block, or a Lua script, terminated its execution after
* being called by a client.
*
* All the keys with at least one client blocked that received at least
* one new element via some PUSH operation are accumulated into
* the server.ready_keys list. This function will run the list and will
* serve clients accordingly. Note that the function will iterate again and
* again as a result of serving BRPOPLPUSH we can have new blocking clients
* to serve because of the PUSH side of BRPOPLPUSH. */
void handleClientsBlockedOnLists(void) {
while(listLength(server.ready_keys) != 0) {
list *l;
/* Point server.ready_keys to a fresh list and save the current one
* locally. This way as we run the old list we are free to call
* signalListAsReady() that may push new elements in server.ready_keys
* when handling clients blocked into BRPOPLPUSH. */
l = server.ready_keys;
server.ready_keys = listCreate();
while(listLength(l) != 0) {
listNode *ln = listFirst(l);
readyList *rl = ln->value;
/* First of all remove this key from db->ready_keys so that
* we can safely call signalListAsReady() against this key. */
dictDelete(rl->db->ready_keys,rl->key);
/* If the key exists and it's a list, serve blocked clients
* with data. */
robj *o = lookupKeyWrite(rl->db,rl->key);
if (o != NULL && o->type == OBJ_LIST) {
dictEntry *de;
/* We serve clients in the same order they blocked for
* this key, from the first blocked to the last. */
de = dictFind(rl->db->blocking_keys,rl->key);
if (de) {
list *clients = dictGetVal(de);
int numclients = listLength(clients);
while(numclients--) {
listNode *clientnode = listFirst(clients);
client *receiver = clientnode->value;
robj *dstkey = receiver->bpop.target;
int where = (receiver->lastcmd &&
receiver->lastcmd->proc == blpopCommand) ?
LIST_HEAD : LIST_TAIL;
robj *value = listTypePop(o,where);
if (value) {
/* Protect receiver->bpop.target, that will be
* freed by the next unblockClient()
* call. */
if (dstkey) incrRefCount(dstkey);
unblockClient(receiver);
if (serveClientBlockedOnList(receiver,
rl->key,dstkey,rl->db,value,
where) == C_ERR)
{
/* If we failed serving the client we need
* to also undo the POP operation. */
listTypePush(o,value,where);
}
if (dstkey) decrRefCount(dstkey);
decrRefCount(value);
} else {
break;
}
}
}
if (listTypeLength(o) == 0) {
dbDelete(rl->db,rl->key);
}
/* We don't call signalModifiedKey() as it was already called
* when an element was pushed on the list. */
}
/* Free this item. */
decrRefCount(rl->key);
zfree(rl);
listDelNode(l,ln);
}
listRelease(l); /* We have the new list on place at this point. */
}
}
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
补一张从http://czrzchao.com/redisSourceBPOP 转过来的图,很好的说明了阻塞中使用的结构:
redisServer都是遍历需求,因此采用list作为存储结构。其中ready_keys需要key加db才能确定一个唯一阻塞值,因此list元素为一个简单的结构体。
redisDb的blocking_keys用于存储单个db的阻塞key,有精确查找需求,采用dict作为基础数据结构。由于db的阻塞key和client为1对多关系,blocking_keys的value为clients的list。
redisDb的ready_keys只是起到一个单纯的去重逻辑,db是key阻塞的单位,因此去重逻辑放在db结构体中最为合适。采用dict存储,将value置为NULL,只用到dict的索引的。
client中的flags和btype用于记录阻塞的一些状态标志,bpop为一个复杂结构体,保存着阻塞超时时间和阻塞keys等,其中keys为dict数据结构,value为NULL,同db的ready_keys。
---------------------
作者:harleylau
来源:CSDN
原文:https://blog.csdn.net/harleylau/article/details/80492660
版权声明:本文为博主原创文章,转载请附上博文链接!
Redis源码剖析--列表t_list实现的更多相关文章
-
Redis源码剖析
Redis源码剖析和注释(一)---链表结构 Redis源码剖析和注释(二)--- 简单动态字符串 Redis源码剖析和注释(三)--- Redis 字典结构 Redis源码剖析和注释(四)--- 跳 ...
-
Redis源码剖析和注释(七)--- 快速列表(quicklist)
Redis 快速列表(quicklist)1. 介绍quicklist结构是在redis 3.2版本中新加的数据结构,用在列表的底层实现. 通过列表键查看一下:redis 列表键命令详解 127.0. ...
-
Redis源码剖析--源码结构解析
请持续关注我的个人博客:https://zcheng.ren 找工作那会儿,看了黄建宏老师的<Redis设计与实现>,对redis的部分实现有了一个简明的认识.在面试过程中,redis确实 ...
-
Redis 源码简洁剖析 02 - SDS 字符串
C 语言的字符串函数 C 语言 string 函数,在 C 语言中可以使用 char* 字符数组实现字符串,C 语言标准库 string.h 中也定义了多种字符串操作函数. 字符串使用广泛,需要满足: ...
-
豌豆夹Redis解决方案Codis源码剖析:Dashboard
豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...
-
玩一把redis源码(一):为redis添加自己的列表类型
2019年第一篇文档,为2019年做个良好的开端,本文档通过step by step的方式向读者展示如何为redis添加一个数据类型,阅读本文档后读者对redis源码的执行逻辑会有比较清晰的认识,并且 ...
-
redis源码(一):为redis添加自己的列表类型
本文档分为三大部分: 环境介绍与效果演示 redis接收命令到返回数据的执行逻辑 代码实现 文档的重点和难点在第三部分,完全阅读本文档需要读者具备基本的c语言和数据结构知识. 环境介绍和效果演示环境介 ...
-
豌豆夹Redis解决方案Codis源码剖析:Proxy代理
豌豆夹Redis解决方案Codis源码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描述: Codis is a proxy base ...
-
如何阅读 Redis 源码?ZZ
原文链接 在这篇文章中, 我将向大家介绍一种我认为比较合理的 Redis 源码阅读顺序, 希望可以给对 Redis 有兴趣并打算阅读 Redis 源码的朋友带来一点帮助. 第 1 步:阅读数据结构实现 ...
随机推荐
-
让wordpress投稿作者在后台只看到自己的文章
wordpress支持多作者撰写,让更多的人参与网站内容的创建是个不错的想法,UGC(User-generated content)使网站主题更丰富,不同的内容吸引不同的受众,一个好的网站应该多产生U ...
-
远程连接mysql数据库,1130问题
远程或使用非127.0.0.1和localhost地址连接时,出现代号为1130问题, ERROR 1130: Host 192.168.2.159 is not allowed to connect ...
-
iOS缓存
存储缓存: 第三方应用只能把信息保存在应用程序的沙盒中.因为缓存数据不是用户产生的,所以它应该被保存在NSCachesDirectory,而不是NSDocumentsDirectory.为缓存数据创建 ...
-
Angular企业级开发(7)-MVC之控制器
1.MVC中的控制器 AngularJS的控制器主要为了把模型和视图连接在一起.大多数业务逻辑操作都会放在视图对应的控制器中.当然如果我们能够把业务逻辑放到后端的REST服务中,就可以开发轻量级Ang ...
-
给xcode项目修改名字
在xcode项目开发中,经常会遇到需要修改项目名字的问题, 但是xcode本身修改项目名字比较麻烦,有时候修改的不完全,有时候修改了项目无法打开,无奈只能建一个新项目.这里提供一种修改xcode项目名 ...
-
程序员的自我修养五Windows PE/COFF
5.1 Windows的二进制文件格式PE/COFF PE文件格式事实上与ELF同根同源,它们都是由COFF格式发展而来. 5.2 PE前身——COFF 在win下,Command Prompt fo ...
-
翻译:TRUNCATE TABLE(已提交到MariaDB官方手册)
本文为mariadb官方手册:TRUNCATE TABLE的译文. 原文:https://mariadb.com/kb/en/truncate-table/我提交到MariaDB官方手册的译文:htt ...
-
x264_param_default分析
{ /* 开辟内存空间*/ memset( param, 0, sizeof( x264_param_t ) ); /* CPU自动检测 */ par ...
-
emwin 之使用键盘数据发送函数的注意事项
@2018-08-08 小记 键盘实现时,在发送键值时, 函数 GUI_SendKeyMsg(GUI_KEY_BACKSPACE, Pressed) 的参数 Pressed 在按键按下状态的 case ...
-
【工具测试】Acunetix 11-登录后扫描的功能
1.概要 在测试的过程中,会给一些只有登录口的测试站点,只有登录后才能访问更多的页面. Acunetix 11的登录后扫描功能摸索了老半天,原来这么神奇.学习了! 2.操作 登录之后 - [Add T ...