Redis 数据结构的底层实现 (一) RealObject,embstr,sds,ziplist,quicklist
一.realObject
Redis使用 string list zset hash set 五大数据类型来存储键和值。在每次生成一个键值对时,都会生成两个对象,一个储存键一个储存值。redis定义了RealObject结构体表示他们
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
1.type
redis 的对象有五种类型,分别是string list zset hash set,type就是用来标识这五种类型的。
/* Object types */
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4
(在redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象)
2.编码类型encoding
redis的对象的实际编码方式由encoding参数指定,也就是ptr指针指向的数据以何种底层实现存放。
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0 /* Raw representation -- 简单动态字符串sds*/
#define OBJ_ENCODING_INT 1 /* Encoded as integer -- long类型的整数*/
#define OBJ_ENCODING_HT 2 /* Encoded as hash table -- 字典dict*/
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap -- 3.2.5版本后不再使用 */
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list -- 双向链表*/
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist -- 压缩列表*/
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset -- 整数集合*/
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist -- 跳表*/
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding -- embstr编码的sds*/
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists -- 由双端链表和压缩列表构成的高速列表*/
可以通过 object encoding key 指令查看值对象的编码
3.访问时间lru
表示该对象最后一次呗访问的时间,占用24bit。保存该值的目的是为了计算对象的空转时长,便于决定是否应该释放该键。
4.引用计数refcount
c语言不具备自动内存回手机制,所以为每个对象设定了引用计数。
- 当创建一个对象时,记为1;
- 当被一个新的程序使用时,引用计数++;
- 不再被一个程序使用时,引用计数--;
- 当引用计数为0时,释放该对象。回收内存。
void decrRefCount(robj *o) {
// 引用计数为小于等于0,报错
if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
// 引用计数等于1,减1后为0
// 须要释放整个redis对象
if (o->refcount == 1) {
switch(o->type) {
// 依据对象类型。调用不同的底层函数对对象中存放的数据结构进行释放
case OBJ_STRING: freeStringObject(o); break;
case OBJ_LIST: freeListObject(o); break;
case OBJ_SET: freeSetObject(o); break;
case OBJ_ZSET: freeZsetObject(o); break;
case OBJ_HASH: freeHashObject(o); break;
default: serverPanic("Unknown object type"); break;
}
// 释放redis对象
zfree(o);
} else {
// 引用计数减1
o->refcount--;
}
}
5.ptr指向实际存放的对象
二、OBJ_ENCODING_RAW
RAW编码方式使用简单动态字符串sds来保存字符串对象
struct sdshdr {
unsigned int len; //buf中已经占用的空间的长度
unsigned int free;//buf中剩余的可用空间长度
char buf[];//数据存放位置
};
- 其中buf[]结束并不依赖于‘\0’,使用len判断结束,可以保存二进制流对象。其中buf[]是一个柔性数组(flexible array member 详见https://blog.csdn.net/sunlylorn/article/details/7544301)
- 预分配,可以减少修改字符串长度增长时造成的再次分配
三、OBJ_ENCODING_EMBSTR
从Redis 3.0 版本开始,字符串引入了embstr编码方式,长度小于OBJ_ENCONDING_EMBSTR_SIZE_LIMIT的字符串将以EMBSTR方式存储。
EMBSTR方式的意思是 embedded string,字符串的空间将会和redisObject对象的空间一起分配,两者分配在同一个内存块中。
redis中内存分配使用的是jemalloc,jemalloc分配内存的时候是按照8,16,32,64作为块的单位进行飞扑的。为了保证采用这种编码方式的字符串能被jemalloc分配在同一个块(chunk)中,该块长度不能超过64,故字符串长度限制OBJ_ENCONDING_EMBSTR_SIZE_LIMIT = 64 - sizeof('\0') -sizeof(robj) -sizeof(sdshdr) =39.
这样可以有效减少内存分配的次数,提高内存分配的效率,降低碎片率。
结构同样适用 sdshdr(见上文)
四、OBJ_ENCODING_ZIPLIST
链表(list),哈希(hash),有序集合(zset)在成员较少,成员值较小的时候都采用压缩链表(ziplist)编码方式进行存储。
这里成员较少,成员值较小的标准可以通过配置项进行设置
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
事实上,ziplist是一个经过特殊编码的双向链表(底层是一个数组),他设计的目的是为了提高存储效率。
一个普通的双向链表,链表中每一项都会占用独立的一块内存,各个项之间用指针连接起来,这样会带来大量的内部碎片(不利于局部性原理缓存加载对应块内存),而且指针也会占用额外的内存。而ziplist将表中每一项存放在前后连续的地址空间内,它其实是一个list而不是一个链表。
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte
+---------+--------+-------+--------+--------+--------+--------+-------+
component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
+---------+--------+-------+--------+--------+--------+--------+-------+
^ ^ ^
address | | |
ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END
|
ZIPLIST_ENTRY_TAIL area |<------------------- entry -------------------->|
+------------------+----------+--------+---------+
component | pre_entry_length | encoding | length | content |
+------------------+----------+--------+---------+
- zlbytes:4bytes 表示ziplist占用的字节总数(包括zlbytes本身占用的4个bytes)
- zltail : 4bytes 表示ziplist表中最优一项(entry)在表中的偏移(字节数)。可以很方便的找到最后一项,从而可以进行O(1)的push和pop
- zllen : 2bytes 表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以能够表示的最大值就是2^16 -1。如果entry中存放的个数小于等于 2^16 -2 ,那么zllen就表示实际的数据个数,如果zllen为全1(>=2^16 -1),那么数据的个数就需要遍历整个entry(也可以取zltail偏移量计算)。
- entry : xbytes 表示真正存放数据的数据项,长度不固定。同时,entry也有自己的内部结构,下文会解释。
- zlend :1bytes ziplist 的结束标记,固定值等于255(全1)。
- 其中 zlbytes zltail zllen等值按照低位编址实现。(实际值:0x12345678 高位编址-0x12345678 低位编址-0x78563412)
entry
- prevrawlen:表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,秩序要向前便宜prevrawlen个字节,就能够找到前一项)。这个字段采用变长编码。
- len:表示当前数据项的长度。采用变长编码
- data:保存数据。
变长编码
prevrawlen: 它有两种可能,要么是1个字节,或者是5个字节
1.如果前面一个数据项字节占用小于等于253,那么prevrawlen就占用一个字节
2.如果前一个entry占用字节数大于等于254,那么第一个字节就是254,后四个字节表示一个整形值,表示prevrawlen的大小。
3.255不出现在entry的第一个字节,因为它表示结束。
len:len字段更加复杂,它根据第一个字节的不同 ,一共分为9种情况。(以下用二进制表示)
1.|00xxxxxx| - 1 byte。第一个字节最高两位是00,那么len字段占1个字节,剩余的6个bit用来表示长度,最多可以表示63.
2.|01xxxxxx|xxxxxxxx| - 2 bytes。 第一个字节最高两位是01,那么len字段占2个字节,剩余的14个bit用来表示长度,最多16383(2^14-1)。
3.|10______|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx| - 5bytes。
第一个字节最高两位是10,len字段占5个字节,总共使用32个bit来表示长度(2-7位舍弃不用),最多表示2^32-1。
(在前三种情况下,data都是按字符串存储的,从下面一种情况开始,开始按整数来存储)。
4.|11000000| - 1 byte。 len字段占用一个字节,后面的data数据储存位2个字节的int16_t类型。
5.|11010000| - 1 byte。 len字段占用一个字节,后面的data存储为4个字节的int32_t类型。
6.|11100000| - 1 byte。 len字段占用一个字节,后面的data存储为8个字节的int64_t类型。
7.|11110000| - 1 byte。 len字段占用一个字节,后面的data存储为3个字节长的整数。
8.|11111110| - 1 byte。len字段占用一个字节,后面的data存储为1个字节长的整数。
9.|1111xxxx| - 1byte。这是一种特殊情况,xxxx从1到13一共13个值,就用这13种情况表示真正的数据。(0001-1101一共13个值表示0-12,这种情况下data于len字段合二为一了。)
hash与zipliist
这个ziplist一共包含33个字节。从byte[0]-byte[32],每个字节的值采用16进制表示。
总结一下,这个ziplist里存了4个数据项,分别为:
字符串:name 字符串:tielei
字符串:age 整数:20
其实这个ziplist是通过两个hset命令创建出来的。
hset user:100 name tielei
hset user:100 age
当我们为某个key第一次执行hset key field value时,redis会创建一个hash结构,这个新创建的hash底层就是一个ziplist。
// object.c
robj *createHashObject(void) {
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
上面的代码负责创建一个新的hash结构,实际上,它创建了一个 type = OBJ_HASH但encoding = OBJ_ENCODING_ZIPLIST的robj对象。
(每当执行一次hset命令,插入的field和value分别作为一个新的数据项插入到ziplist中)。
当然随着数据的插入,hash的层的这个ziplist就可能会转成dict。
ziplist的插入逻辑
在ziplist的entry中插入一段新的数据,会返回一个新的ziplist,替换原来传入的旧的ziplist。因为ziplist是一段连续的地址空间,对他的追加操作,会引发内存的realloc,因此ziplist的内存位置可能会发生变化。
1.先把插入结点后面结点的prevrawlen写入新entry 然后把新结点的长度写入后一个结点的prevrawlen
2.然后计算插入后的空间大小,调用allocator的zrealloc,数据拷贝。
3.然后就是将插入位置后的数据向后挪动,插入新entry。
五、OBJ_ENCODING_LINKEDLIST 和 OBJ_ENCODING_QUICKLIST
在redis 3.2之前 一般的链表采用LINKEDLIST编码。
在redis 3.2版本开始,所有的链表都采用QUICKLIST编码。
两者都是使用基本的双端链表数据结构,区别是QUICKLIST每个结点的值都是使用ZIPLIST进行存储的。
// 3.2版本之前
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr,void *key);
unsigned long len;
} list;
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
// 3.2版本
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned int len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
什么意思呢,比如,一个包含三个结点的quicklist,如果每个结点的ziplist又包含四个数据项,那么对外表现上,这个list就总共包含12个数据项。这样的设计,实际上是对于时间和空间的一种折中。
- linkedlist便于在表的两端进行push和pop操作,但是它的内存开销较大。首先,它的每个节点除了要保存数据之外还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,容易产生内存碎片,还容易造成抖动。
- ziplist由于是一整块连续的内存,存储效率很高,但不利于添加和删除操作,每次都会重新realloc,尤其是当ziplist很长的时候,一次realloc造成的开销特别的大,查询的开销也特别的大。
于是quicklist集合了两个结构的有点,但多少是合理的长度呢,redis系统中用户可以自定义这个值。
list-max-ziplist-size -2
这个参数可正可负,取正值 n 的时候,这个正值表示的就是每个ziplist的长度最多不能超过n。
当取复制的时候 只能取 -1 ~ -5 这五个值,表示按照字节数来限定每个ziplist节点的长度。
- -1 每个quicklist节点大小不能超过4Kb
- -2 每个quicklist节点大小不能超过8Kb
- -3 每个quicklist节点大小不能超过16Kb
- -4 每个quicklist节点大小不能超过32Kb
- -5 每个quicklist节点大小不能超过64Kb
节点的压缩
另外,quicklist的设计目标是用来存储很长的数据链表的,当链表很长的时候,最容易被访问的是两端的数据,中间的数据被访问的概率比较低,如果应用场景符合这个特点,list还提供了一个选项,可以把中间的数据节点进行压缩,而两边不被压缩,参数
list-compress-depth 0
就是用来完成这个设置的,数值表示两边不被压缩的节点个数
- 其中 0 是个特殊值,表示都不压缩,这是 redis的默认值。
- 1表示quicklist两端各有1个节点不压缩,中间的节点压缩。
- 2表示quicklist两端各有2个节点不压缩,中间的节点压缩
- 3......
- 对于quicklist的压缩算法,采用LZF---一种无损压缩算法。https://www.cnblogs.com/pengze0902/p/5998843.html
quicklist的数据结构(quicklist.h)
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF; /* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned int len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
quicklistNode结构代表quicklist的一个节点
- prev 指向前一个节点
- next 指向后一个节点
- zl 数据指针。如果当前节点没有被压缩,它指向一个ziplist;否则,它指向一个quicklistLZF结构
- sz 表示zl指向的ziplist的总大小,如果他被压缩了,那么它指压缩前的ziplist大小。
- count表示ziplist里面包含的数据项的个数,
- encoding 表示ziplist是否被压缩
- container 保留字段,表示一个quicklistNode是直接存数据,还是使用ziplist存数据,或者采用其他结构类存数据。但是在目前实现中是一个固定值2,表示ziplist存数据
- recompress 这个节点之前被压缩没有,当我们使用lindex这样的命令查看某一项本来被压缩的数据时,需要把数据暂时解压,这时就设置recompress=1 做一个标记,等空闲时候再次把数据重新压缩。
- attemped_compress
- extra 其他扩展字段 目前弃用
quicklistLZF结构表示一个被压缩的ziplist。
- sz 表示压缩后的ziplist大小
- compress 柔性数组(flexible array member) 存放数据压缩后的ziplist字节数组
quicklist是真正的保存quicklist的结构
- count 所有ziplist数据项的总和
- len quicklistNode节点的个数
- fill ziplist大小的摄者 存放 list-max-ziplist-size
- compress 节点压缩深度 存放 list-compress-depth
上图为一个quicklist结构图 对应的fill和compress 配置 为
list-max-ziplist-size 3
list-compress-depth 2
其中左右两端各有两个黄色节点,是没有被压缩的,他们的数据指针zl直接指向ziplist结构。中间的蓝色节点是lzf压缩过的,zl指针指向quicklistLZF结构。
左侧头结点上的ziplist有两项数据,右侧头结点有1项。
quicklist插入
- 头尾节点插入时,如果对应节点的ziplist大小没有超过限制,则新数据直接被插入对应ziplist;如果超过限制,那么新建一个quicklistNode节点,把待插入节点插入新的ziplist中。
- 如果插入的位置是链表中间部分,当插入位置所在的ziplist没达到大小限制,直接插入对应ziplist;
- 当所在的ziplist大小超过了限制,但插入的位置在ziplist两端,如果相邻节点的ziplist没有超过大小限制,那么就插入相邻节点
- 如果相邻节点的大小超过了限制,那么新建一个quicklistNode,插入对应节点
- 对其他情况,需要吧当前ziplist分裂成两个节点,然后在其中一个节点中插入数据。
Redis 数据结构的底层实现 (一) RealObject,embstr,sds,ziplist,quicklist的更多相关文章
- Redis 数据结构的底层实现 (二) dict skiplist intset
一.REDIS_INCODING_HT (dict字典,hashtable) dict是一个用于维护key和value映射关系的数据结构.redis的一个database中所有的key到value的映 ...
- Redis数据结构底层知识总结
Redis数据结构底层总结 本篇文章是基于作者黄建宏写的书Redis设计与实现而做的笔记 数据结构与对象 Redis中数据结构的底层实现包括以下对象: 对象 解释 简单动态字符串 字符串的底层实现 链 ...
- 面试官:你看过Redis数据结构底层实现吗?
面试中,redis也是很受面试官亲睐的一部分.我向在这里讲的是redis的底层数据结构,而不是你理解的五大数据结构.你有没有想过redis底层是怎样的数据结构呢,他们和我们java中的HashMap. ...
- Redis(一) 数据结构与底层存储 & 事务 & 持久化 & lua
参考文档:redis持久化:http://blog.csdn.net/freebird_lb/article/details/7778981 https://blog.csdn.net/jy69240 ...
- Redis 数据结构与内存管理策略(上)
Redis 数据结构与内存管理策略(上) 标签: Redis Redis数据结构 Redis内存管理策略 Redis数据类型 Redis类型映射 Redis 数据类型特点与使用场景 String.Li ...
- 5种Redis数据结构详解
本文主要和大家分享 5种Redis数据结构详解,希望文中的案例和代码,能帮助到大家. 转载链接:https://www.php.cn/php-weizijiaocheng-388126.html 2. ...
- Redis数据结构和对象三
1.Redis 对象系统 Redis用到的所有主要数据结构,简单动态字符串(SDS).双端链表.字典.压缩列表.整数集合.跳跃表. Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些 ...
- 选择合适Redis数据结构,减少80%的内存占用
redis作为目前最流行的nosql缓存数据库,凭借其优异的性能.丰富的数据结构已成为大部分场景下首选的缓存工具. 由于redis是一个纯内存的数据库,在存放大量数据时,内存的占用将会非常可观.那么在 ...
- 高性能的Redis之对象底层实现原理详解
对象 在前面的数个章节里, 我们陆续介绍了 Redis 用到的所有主要数据结构, 比如简单动态字符串(SDS).双端链表.字典.压缩列表.整数集合, 等等. Redis 并没有直接使用这些数据结构来实 ...
随机推荐
- 10.HanLP实现k均值--文本聚类
笔记转载于GitHub项目:https://github.com/NLP-LOVE/Introduction-NLP 10. 文本聚类 正所谓物以类聚,人以群分.人们在获取数据时需要整理,将相似的数据 ...
- 【新人赛】阿里云恶意程序检测 -- 实践记录10.13 - Google Colab连接 / 数据简单查看 / 模型训练
1. 比赛介绍 比赛地址:阿里云恶意程序检测新人赛 这个比赛和已结束的第三届阿里云安全算法挑战赛赛题类似,是一个开放的长期赛. 2. 前期准备 因为训练数据量比较大,本地CPU跑不起来,所以决定用Go ...
- magento2.2.3 根据产品ID获取栏目名称的正确调用方式
根据product_id 获取 category_ids : /** * @param $product_id * @return array */ public function mc_getCat ...
- tensorflow数据集加载
本篇涉及的内容主要有小型常用的经典数据集的加载步骤,tensorflow提供了如下接口:keras.datasets.tf.data.Dataset.from_tensor_slices(shuffl ...
- Android电源管理基础知识整理
前言 待机.睡眠与休眠的区别? Android开发者官网当中提到"idle states",该如何理解,这个状态会对设备及我们的程序造成何种影响? 进入Doze模式中的idle状态 ...
- 剑指offer-面试题27-二叉树的镜像-二叉树
/* 题目:输入一个二叉树,输出该函数的镜像. */ /* 思路: 基础条件:树为空,或只有一个节点. 其它:递归交换二叉树的左右子树. */ void Mirror(TreeNode *pRoot) ...
- Mac中如何搭建Vue项目并利用VSCode开发
(一)部署Node环境 (1)下载适合Mac环境的Node包,点击进入下载页面 (2)安装Node环境:找到下载好的Node包,这里是node-v12.14.1.pkg,我们双击它,会进入Node.j ...
- STL与基本数据结构
目录 Vector list -- 链表 Stack -- 栈 queue -- 队列 优先队列 -- priority_ queue set -- 集合 multiset map 这是我第一次用Ma ...
- hdu 1087 Super Jumping!(类最长上升子序列)
题意:在一组数中选取一个上升子序列,使得这个子序列的和最大. 解:和最长上升子序列dp过程相似,设dp[i]为以第i位为结尾最大和,那么dp[i]等于max(dp[0],dp[1],,,,,dp[i- ...
- xlrd模块-读取Execl表格
#xlrd模块 读取execl表格 import xlrd Execl = xlrd.open_workbook(r'Z:\Python学习\python26期视频\day76(allure参数.读e ...