一起来读懂 Redis 的设计与实现 - 数据结构
准备将之前攒下的书先看一遍,主要是有个大概的了解,以后用的时候也知道在哪里找。所以准备开几篇共读的帖子,激励自己多看一些书。
Redis 基于 简单动态字符串
(SDS)、双端链表
、字典
、压缩列表
、整数集合
等基础的数据结构,创建了一个对象系统,这个对象系统包含:字符串对象
(String)、列表对象
(List)、集合对象
(Set)、有序集合对象
(Zset)、哈希对象
(Hash) 5种数据对象类型。但是这5种对象类型,其内部的基础的存储结构 并不是 一对一的一种,而是每一种包含了至少两种数据结构。
我们这篇主要用来说一下其基础的存储结构
前提条件
redis 底层是使用C语言编写的,所以很多函数直接使用的C库。
一、SDS(简单动态字符串)
我们知道C语言中字符串 是以字符数组char[]
进行存储的,字符串的结束是以 空字符‘/0’
来进行标识的,也就是字符串的实际长度比我们看见的字符串都会多1 byte(字节)
。
如果我们想要查看一下字符串的长度,那么就需要遍历一下字符数组,时间复杂度为O(n)。
1.1 结构说明:
- redis中使用结构体
SDS
用来存储字符串类型,同样的使用字符数组
进行存储 也自带空字符‘/0’
,从而可以使用C语言中字符串相关的特性/函数。 - len:数组已用长度记录,就是说字符串的真实长度(不算‘/0’)
- free:数组中剩余可用长度,也就是数组中还有多少长度使用的。
1.2 内存预分配
我们从SDS结构图可以知道SDS中字符数组的长度是和字符串长度不一样的,那么这个长度是如何分配的?
- 首先如果是创建/扩展:
- 小于1M,分配的 未使用内存 是 使用内存的
2倍
- 大于1M,那么 每次扩展未使用内存为
1M
- 小于1M,分配的 未使用内存 是 使用内存的
- 如果是收缩:
并不会立即真正释放
,会留下未使用的内存,可以通过Api来进行释放,从而避免内存泄漏
。
1.3 二进制
由于C语言中字符串以 ‘/0’标识结尾,所以C语言中字符串不能存储 图片、音视频的二进制数据,但是redis 中字符串以len来做为结尾的判断,所以可以使用字符串来存储二进制的数据。
当然对于 文本类型的 本身结束就是‘/0’结尾的,所以我们可以直接使用C的字符串特性。
1.4 特性(总结):
- 自带空格,从而可以使用C语言字符串相关特性
-
存储
使用空间
和未使用空间
这样长度可以快速得出(时间复杂度O(1)
),不用遍历数组(时间复杂度O(n)
) - 由2我们可以杜绝 C语言中
缓存溢出
的问题 - 节省了避免缓存溢出而带来 内存重分配的系统开销
- 空间预分配
- 扩展:小于1M 预分配未使用空间为 使用空间的2倍,大于1M,预分配未使用空间为1M;
- 收缩:惰性空间释放
- 可以存储图片和音视频二进制数据。
关于 C语言缓存溢出:
我们知道数组是一块内存挨着的存储空间,C语言中,如果我们直接对字符串增加,会有如下这种情况的发生:
现在给hello 尾部添加 “-wi” 字符串
"字符串“hello”添加 "-wi" 字符串之后内存快照"所以C语言中我们为了防止这种情况,每次扩展的时候都会进行
内存重分配
,使得空余的字符数组可以容得下我们新加的字符串。但是内存重分配
会导致系统调用,对于redis这种频繁增加删除的数据库来说,这种肯定要尽可能的减少系统性能的浪费。
二、链表
其实就是一个结构体
持有双向链表
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void *(*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}list;
特性
- 双向连表,这样查找前(或者后)一个节点,复杂度为O(1)
- 有头尾指针,查找第一个节点、最后一个节点复杂度为O(1)
- 带链表长度计数器,返回长度复杂度为O(1)
- 无环(⚠️)
-
void*
存储节点的值,可以使用dup\free\match 等特定函数。
三、字典
C语言本身没有 字典
类型,但是对于key-vale 这种映射的关系 在redis是常用的,所以redis 自己构建了一个结构体,本身使用的是 hash 结构
typedef struct dict {
dictType *type; //dictType也是一种数据结构,dictType结构中包含了一些函数,这些函数用来计算key的哈希值,进而用这个哈希值计算key在dictEntry型table数组中的下标
void *privdata; //私有数据,保存着dictType结构中函数的参数
dictht ht[2]; //两张哈希表:一张用来正常存储节点,一张用来在rehash时临时存储节点
long rehashidx; //rehash的标记:默认-1,当table数组中已有元素个数增加/减少到一定量时,整个字典结构将进行rehash给每个table元素重新分配位置,rehashidx代表rehash过程的进度,rehashidx==-1代表字典没有在进行rehash,rehashidx>-1代表该字典结构正在对进行rehash
} dict;
3.1 字典结构体
- dictType:也是一种数据结构,dictType结构中包含了一些函数(dup\free等),这些函数用来计算key的哈希值,进而用这个哈希值计算key在dictEntry型table数组中的下标。
说白了,也就是redis 的字典为每种基础类型都创建了一个dictType,使得可以使用类型特定的函数
- privdata:私有数据,存储dictType构造参数,不同的类型传不同 的参数
-
ht[]:哈希表,真正存储数据的地方。其中
ht[0]
是使用的表,ht[1]
是没有分配内存空间
,只有在rehash
的时候会分配内存,用到。 -
rehashidx:在
rehash
的时候才会使用。
3.1.1 redis 哈希表结构体:
typedef struct dictht { //哈希表
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
说明:
- table 是hash 存储的
桶
数组地址 - size 是桶的大小,也就是数组的容量
- sizemask,进行hash 运算的时候会使用到,一般为 size-1;(用于计算每个key在table中的下标位置=hash(key)&sizemask)
- 记录哈希表的table中已有的节点数量(节点=dictEntry=键值对)。
3.1.2 redis的hash节点结构体
typedef struct dictEntry {
void *key;//键
union{ //值
void *val;//值可以是指针
uint64_tu64;//值可以是无符号整数
int64_ts64;//值可以是带符号整数
} v;
struct dicEntry *next;//指向下个dictEntry节点:redis的字典结构采用链表法解决hash冲突,当table数组某个位置处已有元素时,该位置采用头插法形成链表解决hash冲突
} dictEntry;
3.2 结构图
3.3 hash 步骤
- 算出key 的hash 值(通过key 自身的函数)
- 使用
步骤1
得到的 哈希值 和sizemask
进行运算index = hash & dict->ht[x].sizemask;
得到要存储的索引位置。
其实和java 的hashmap 运算过程一样
当然这种肯定会遇到hash 冲突
,这时候就是用 链地址法
解决冲突
也为了插入效率
问题(插入的话还需要遍历在数组后面的链表),采用头插法
3.4 rehash 步骤
所谓的rehash
就是当前hash 结构
(主要是 桶数组
)已经低于某种效率了,需要进行优化,从而 再次
进行hash运算
- 给ht[1]分配内存,具体的分配规则:
- 如果是
扩展(增加值)
导致的rehash,分配的ht[1]内存为:h[0].user*2
的2^n
(2的n次幂
) - 如果是
收缩
导致的rehash,分配的ht[1]内存为:h[0].user
的2^n
(2的n次幂
)
- 如果是
- 将
rehashidx
赋值0 - 将
ht[0]
的 值重新
hash 运算到ht[1]
中去,运行一次rehashidx
值+1
- 将
ht[0]释放
,将ht[1]
改为ht[0]
,新建
一个ht[1]
3.5 rehash 触发条件
- 没有在执行
BGSAVE
命令或者BGREWRITEAOF
命令,并且哈希表
的负载因子
大于或等于1
。 - 目前
正
在执行BGSAVE
命令或者BGREWRITEAOF
命令,并且哈希表的负载因子
大于或等于5
。 - 当哈希表的
负载因子``小于``0.1
时,redis会自动开始对哈希表进行缩容操作。
说一下负载因子
:节点数/桶大小
3.6 渐进rehash
对于数量小的hash表进行 reash 一次执行就ok ,但是数据量特别大
的呢?那种成千上万几亿的数据
,这种如果进行一次性
的rehash的话占用资源
是非常大的,此时redis 就要处于不可用
的状态了,这种是绝对不允许的,所以这种是需要分批次来进行rehash,就是渐进rehash
。
对于这种有个注意点:
如果在rehash 的时候写入
数据,那么我们直接写到ht[1]
上,
但是如果是读
、更新
、删除
操作 则是两个ht[]
都要用
3.7 特性
-
ht[0]
为一般存储,ht[1]
为rehash
时使用的存储 -
rehashidx
开始为-1
,开始rehash的时候会变成0
- hash 算法是
MurMurHash
- 通过
链地址法
解决冲突 - 采用
头插法
- 使用
渐进rehash
触发条件
四、跳跃表
对于一个有序
数组,我们想要快速访问
,并且频繁
的更新数据
,那么我们会使用什么样的存储操作呢?对于 有序
这两个字 我们快速访问
肯定想到的是二分表
,树形
结构,尤其是 二叉平衡树
最为可靠,但是二叉平衡树
以及它的简易替代 红黑树
在数据库
这种 更新
比较频繁
的应用中,维持
他们的平衡
是很耗费性能
的。所以redis 采用了相似的 跳跃表
这种结构。
不同于
前面几种结构,跳跃表 只是在存储大量的有序
数组中 或者 redis 内部结构中使用到了。
本意是减少复杂度
,替代平衡树
,并且因为跳跃表
的实现比平衡树
要来得更为简单
,所以有不少程序都使用跳跃表来代替
平衡树。
结构图:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //header指向跳跃表的表头节点,tail指向跳跃表的表尾节点
unsigned long length; //记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
int level; //记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
} zskiplist;
typedef struct zskiplistNode {
robj *obj; /*成员对象*/
double score; /*分值*/
struct zskiplistNode *backward; /*后退指针*/
struct zskiplistLevel { /*层*/
struct zskiplistNode *forward; /*前进指针*/
unsigned int span; /*跨度*/
} level[];
} zskiplistNode;
说明:
这里说一下跳跃表的思想:
我们在有序列表中查找一个数,
对于**数组**
那么我们就可以使用二分法
去查找
,以此来提高查找效率
,但是如果我们要频繁的插入新数据,那就要不断的去移动这个数组的数据,这样来说数据如果特别大性能并没有得到很大的提升(移动数据数据相比查找来说是更耗时的)
对于**链表**
来说,我们的插入和删除
就比较方便了,毕竟只有指针
之间引用的修改
,这不提高效率了么?但是链表是不可以用二分法的(中间元素需要遍历才能找到O(n)
,数据可以直接访问O(1)
),我们有没有办法去提高链表的查找效率呢?
我们可以每隔
一个节点在上面建立
一个节点,也就是新的链表
是之前链表
的一半
的数量,查找的时候以新链表
为起点
,遇到比当前节点大(小)比后一节点小(大)的就移动到之前节点去查找,这样查找的效率可以得到很大的提升,当然,我们可以在新链表上在建立一层,查找速度比之前的在提高一些,然后在新建一层……这样最终就是一个建立索引的过程。
对于 二分规则
(每隔一个节点建立上一层索引) 是否要完美``执行
?
当最后我们建立好之后是不是发现,每隔一个建立这种索引的过程是不是和平衡二叉树
有点像啊?而且有个最重要的是,当我们插入新数据的时候,为了维持每隔一个建立上一层索引的概念,我们不得不更新索引。。这样当索引数量大的时候不又产生效率问题么,似乎也没办法解决了??
既然有这种问题,我们就没必要严格执行二分不就行了么。关注一下我们的 目的 只在于让查找的效率提升,那么我们按照这种方法 提升查找效率,既然不能达到百分百完美,那我们就尽量的靠近实现二分就行。
用数学统计学中 的概率问题
去解决,也就是实现平均的二分
其实查找效率
就能够得到提升
,所以并不是严格执行每隔一个进行一次建立索引。
特性:
- 同样的,跳跃表也是redis 建立了一个
结构体
来持有
节点对象,这样我们使用的时候可以使用length
来获取长度
,level
获取最大层数
、以及头节点
、尾节点
,这些获取的时候复杂度都是O(1) - 然后 每个 listnode 节点都有
多个``前进指针
和一个``后退指针
-
前进指针
指向 比节点大(或者小)的下个节点;(也就是指向尾部
元素 的方向
) -
后退指针
指向 当前节点的 上一层级(只有一个,并且指向上一层级,不能跨级) - 我们访问或者查找元素时 通过 前进指针就可以查找到。
- 随机层数:对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,Redis 跳跃表默认允许最大的层数是 32
五、整数集合
5.1 结构体
//每个intset结构表示一个整数集合
typedef struct intset{
//编码方式
uint32_t encoding;
//集合中包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
整数集合也是一样的持有一个整数数组的 结构体,结构体中存储 数组长度、数组类型
-
contents[]:是整数集合的
底层
实现,整数集合的每个元素都是 contents数组的个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。 - length:属性记录了数组的长度。
-
encoding:
intset结构体
将contents属性
声明为int8_t
类型的数组,但实际
上 contents数组并不保存
任何int8t类型的值, contents数组的真正
类型取决于encoding属性
的值。encoding属性的值为INTSET_ENC_INT16则数组就是uint16_t类型,数组中的每一个元素都是int16_t类型的整数值(-32768——32767),encoding属性的值为INTSET_ENC_INT32则数组就是uint32_t类型,数组中的每一个元素都是int16_t类型的整数值(-2147483648——2147483647)。
5.2 升级
C语言中,内存
是需要我们自己
行进管理
的。其实我们可以知道我们储存
的时候并不是一次性
存储的,可能之前储存的是 int8 类型的,后来数据发生变化
,我们储存int16甚至int32\int64类型的,为了防止这种情况发生,我们一般一开始就进行 int64的定义存储,这样我们就不用担心后面使用的时候发生内存溢出
问题。但是有个问题是:我们这样做的话,假如前面的都是int8的,后面int64 最后很晚才入库 或者直接不入库了,这样我们用int64存储的int8数据,这不是内存浪费
么?所以,redis 为了这种情况,对没有超过当前存储的情况使用当前结构进行存储,也就是开始就是 int8,等到进来一个数发现存储不够,需要int16\int32 那么我在升级 整个集合的类型。从而避免了资源的浪费。
升级首先
要做的就是空间重分配
。
只有升级
操作,没有``降级
操作。
5.3 优点
灵活性:就是我的存储可以更加的灵活,不必担心类型转换的问题。
节约内存:不必一开始就建立大容量的数据。
六、压缩列表
它是我们常用的 zset ,list 和 hash 结构的底层实现之一
和其他类型一样,压缩列表也是由一个结构体来持有存储的数据数据,然后存储了数组中节点的数量,节点的偏移量,节点的存储大小。
其中,entry[] 存储的是有序
的数组序列。
entry[]
我们重点看一下entry[]
的结构体
为什么小数据量使用
我们知道,对于内存的读取来说 顺序读取 是比 随机读取 效率要高很多的所以对于读取的操作,我们常常会将其设置为数组,提高其读取效率。但是如果是更新来说,大数据量的数组往往是效率不可靠的。所以,我们也就明白为什么 对于压缩列表来说,只有小数据量的才会使用。
encoding
——解决空间浪费问题
对于数据存储也有一个问题:就是我们在整数集合中说的,如果前面的数据是int8 的后面的是int64的,这样我们的存储空间就要设置成64的,前面不就浪费了很多内存么,如何解决这个问题?
我们可以存储成不同
结构类型的 啊,比如entry 结构体,我的content 就是不同数据类型的,这样存储的时候小的存储成int8 大的存储成int64,但是这样会有个问题:我们在遍历它的时候由于不知道每个元素的大小是多少,因此也就无法计算
出下一个节点
的具体位置,如果前面读取的是in8 后面读取的int64 我怎么分开呢?
这个时候我们可以给每个节点增加一个encoding
的属性,我们就可以知道这个**content**
中记录数据的格式,也就是内存的大小了。
一字节、两字节或者五字节长, 值的最高位为 00 、 01 或者 10 的是字节数组编码: 这种编码表示节点的 content 属性保存着字节数组, 数组的长度由编码除去最高两位之后的其他位记录;
一字节长, 值的最高位以 11 开头的是整数编码: 这种编码表示节点的 content 属性保存着整数值, 整数值的类型和长度由编码除去最高两位之后的其他位记录;
如此。我们在遍历节点的之后就知道每个节点的长度(占用内存的大小),就可以很容易计算出下一个节点再内存中的位置。这种结构就像一个简单的压缩列表了。
previous_entry_length
我们知道如何顺序读取了,但是如果我想后退读取数据呢?我们不知道前面数据的类型 大小,怎么取截取内存读取呢?
和encoding 一样,我们记录一下上一个entry的大小,然后用当前内存地址-**previous_entry_length**
如此就能计算出上一个内存地址,然后按照相应规则读取了。
这个属性记录了压缩列表前一个节点的长度,该属性根据前一个节点的大小不同可以是1个字节或者5个字节。
如果前一个节点的长度小于254个字节,那么previous_entry_length的大小为1个字节,即前一个节点的长度可以使用1个字节表示
如果前一个节点的长度大于等于254个字节,那么previous_entry_length的大小为5个字节,第一个字节会被设置为0xFE(十进制的254),之后的四个字节则用于保存前一个节点的长度。
连锁更新
由上述我们知道,下一个节点存储上一个节点的大小,如果我们添加节点 或者 删除节点的时候,节点的大小发生了变化:
考虑下这种情况:
比如多个连续节点长度都是小于254 字节的,都处于 250 和253 字节之间,现在我们在前面插入一个大于254 字节长度的节点,那么后一节点 之前的 1字节 显然不能满足,只能更改为 5 字节来尽心存储 大于254 字节的长度,我们在看后面,麻烦的事情来了:我们将previous_entry_length 改成5字节的长度,那么我们当前节点就超过了254节点,显然下一节点的previous_entry_length也不满足了,然后我们就又要改,这样一系列的问题就出现了。这样的问题称为 连锁更新。
尽管连锁更新的复杂度较高,但是它真正造成性能问题的几率是很低的:
- 要很多连续的,长度介于 250和253 之间的节点
- 即使出现连锁更新,但是如果只是小范围,节点数量不多,就不会造成性能影响。
所以在实际中我们可以放心的使用这个函数。
对象系统
到这里我们已经将redis 的存储结构讲完了,但是对象系统和 存储结构之间具体的关系,或者说联系是什么呢?
首先我们明白,在对象系统中,redis 有五
大对象:STRING
、LIST
、SET
、ZSET
、HASH
然后每个对象 的底层存储是 我们上面说的哪几种类型,
说白了就是说的 java中的基本类型
和对象
之间的关系。
从上面我们知道每种对象都至少 有两种 基本类型,那么他们之间的划分 或者说界限是什么呢?
1. 界限
2. 各种对象API
STRING API
LIST API
SET API
ZSET API
HASH API
3. 公共Api
4. 类型检查
我们知道对于redis 来说,每个对象都使用了至少两种基本类型,但是C 语言中,如果类型不一样,常常会出现类型错误的问题。我们怎么解决呢?
这里我们看一下对象的储存结构:
struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
void *ptr; //执行底层实现数据结构的指针
int refcount; //引用计数,用于内存回收
unsigned lru:22; // 记录最近一次访问这个对象的时间
}
通过这个我们可以看到,其实redis 对象存储了使用的基本结构,这样我们使用api的时候,都会进行一个类型检查然后再去进行使用,对于非本类型的 api 返回错误信息。
其实每个对象内部基本类型的转换也是需要注意一下的,就是边界。
5. 多态性
我们可以从 公共api 中可以看到 redis 对象的多态性,就是不同的类型执行的 方法结果是一样的,只不过对于不同的类型都有自己特殊的处理
其实这里的多态性在我们同一个类型中不同基础结构的 API 中也是有体现的。
6. 内存回收/引用计数器
C语言中,内存是交给我们自己来进行管理的,所以当我们不使用这块内存的时候就要就行内存释放。我们怎么知道内存是否还在使用呢?从之前我们对象结构中可以看到,redis 维护了一个 引用计数器,这样我们每次引用的时候都会 使得 refcount+1。其实引用计数器在很多 语言中都有使用java中也使用过,这里面有个比较难受的点:如果两个对象之间相互引用,但是两个都是没有用的,这种永远不会是0,也就就释放不了拉。在redis 中还维护了一个lru
就是说设置一个时间,超过这个时间的,那么就强制释放它,这样就避免了相互引用导致的 内存释放问题。
7. 对象共享
redis 大量用到了sds 这种结构,而且可以在其他基本结构中 嵌套使用。例如链表的节点的值可以使用 sds 。我们如果有很多一样的数据,如果在内存中分配一个空间,少量的还行如果数量多了岂不会“浪费”?
所以redis 采用了对象共享,也就是这个类型的数据如果在内存中已经有了,那么我们再次创建的时候不会开辟新的空间,直接使用对象的引用,此时引用计数器+1,那么数据量大的时候就会节省很多内存。
redis 服务器启动
的时候会创建一万个字符串对象
,这些对象包含0-9999
字符串对象,以后使用的时候不在创建新的而是使用这个对象。
参考资料
《Redis设计与实现》-黄健宏
部分图片来与百度搜索
上一篇: 海钩沉浮史 - 漫谈中国麻将文化
下一篇: CLIP、GLIP 论文解释,简明扼要
推荐阅读
-
带您阅读 "新一代垃圾收集器 ZGC 的设计与实现 "之一:垃圾收集器概述
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。
-
什么是可用性测试?有效性(Effectiveness)-- 用户完成特定任务和实现特定目标的正确性和完整性程度;效率(Efficiency)-- 用户完成任务的正确性和完整性程度与所用资源(如时间)之比;满意度(Satisfaction)-- 用户在使用产品时的主观满意度和接受程度。 2.如何获得可用性? 可以参考以下原则:Gould、Boies 和 Lewis(1991 年)为以用户为中心的设计定义了 4 个重要原则: 早期以用户为中心:设计者应在设计过程的早期就努力了解用户的需求。 综合设计:设计的所有方面都应同步发展,而不是按顺序进行。使产品的内部设计始终与用户界面的需求保持一致。 早期和持续测试:当今唯一可行的软件测试方法是经验主义方法,即如果实际用户认为设计可行,该设计就是可行的。通过在整个开发过程中引入可用性测试,用户就有机会在产品推出之前对设计提出反馈意见。 迭代设计:大问题往往掩盖了小问题的存在。设计人员和开发人员应在整个测试过程中对设计进行迭代。 3...什么是可用性测试? 可用性测试是根据可用性标准对图形用户界面进行的系统评估。 可用性测试是衡量用户与系统(网站、软件应用程序、移动技术或任何用户操作设备)交互时的体验质量。4.如何进行可用性测试? l 实验室实验
-
一起来读懂 Redis 的设计与实现 - 数据结构
-
小红书大产品部架构 小红书产品概览--经过性能、稳定性、成本等多个维度的详细评估,小红书最终决定选择基于腾讯云星海自研硬件的SA2云服务器作为主力机型使用。结合其秒级的快速扩缩、超强兼容和平滑迁移能力,小红书在抵御上亿次用户访问、保证系统稳定运行的同时,也实现了成本的大幅降低。 星海SA2云服务器是基于腾讯云星海的首款自研服务器。腾讯云星海作为自研硬件品牌,通过创新的高兼容性架构、简洁可靠的自主设计,结合腾讯自身业务以及百万客户上云需求的特点,致力于为云计算时代提供安全、稳定、性能领先的基础架构产品和服务。如今,星海SA2云服务器也正在为越来越多的企业提供低成本、高效率、更安全的弹性计算服务。 以下是与小红书SRE总监陈敖翔的对话实录。 问:请您介绍一下小红书及其主要商业模式? 小红书是一个面向年轻人的生活方式平台,在这里,他们发现了向上、多元的真实世界。小红书日活超过 3500 万,月活跃用户超过 1 亿,日均笔记曝光量达 80 亿。小红书由社交平台和在线购物两大部分组成。与其他线上平台相比,小红书的内容基于真实的口碑分享,播种不止于线上,还为线下实体店赋能。 问:围绕业务发展,小红书的系统架构经历了怎样的变革和演进? 系统架构变化不大,影响最深的是资源开销。过去三年,资源开销大幅增加,同比增长约 10 倍。在此背景下,我们努力进行优化,包括很早就开始使用 K8S 进行资源调度。到 18 年年中,绝大多数服务已经完全实现了容器化。 问:目前小红书系统架构中的计算基础设施建设和布局是怎样的? 我们目前的建设方式可以简单描述为星型结构。腾讯云在上海的一个区是我们的计算中心,承载着我们的核心数据和在线业务。在外围,我们还有两个数据中心进行计算分流,同时承担灾备和线上业务双活的角色。 与其他新兴电子商务互联网公司类似,小红书的大部分计算能力主要用于线下数据分析、模型训练和在线推荐等平台。随着业务的发展,对算力的需求也在加速增长。
-
反传销网8月30日发布:视频区块链里的骗子,币里的韭菜,杜子建骂人了!金融大V周召说区块链!——“一小帮骗子玩一大帮小白,被割韭菜,小白还轮流被割,割的就是你!” 什么区块链,统统是骗子 作者:周召(知乎金融领域大V,毕业于上海财经大学,目前任职上海某股权投资基金合伙人) 有人问我,区块链现在这么火,到底是不是骗局? 我的回答是: 是骗局。而且我并不是说数字货币是骗局,而是说所有搞区块链的都是骗局。 -01- 区块链是一种鸡肋技术 人类社会任何技术的发明应用,本质都是为了提高社会的生产效率。而所谓区块链技术本质不过是几种早已成熟的技术的大杂烩,冗余且十分低效,除了提高了洗钱和诈骗的效率以外,对人类社会的进步毫无贡献。 真正意义上的区块链得包含三个要素:分布式系统(包括记账和存储),无法篡改的数据结构,以及共识算法,三者互为基础和因果,就像三体世界一样。看上去挺让人不明觉厉的,而经过几年的瞎折腾,稍微懂点区块链的碰了几次壁后都已经渐渐明白区块链其实并没有什么卵用,区块链技术已经名存实亡,沦为了营销工具和传销组织的画皮。 因为符合上述定义的、以比特币为代表的原教旨区块链技术,是反效率的,从经济学角度来说,不但不是一种帕累托改进,甚至还可以说是一种帕累托倒退。 原教旨区块链技术的效率十分低下,因为要遍历所有节点,只能做非常轻量级的数据应用,一旦涉及到大量的数据传输与更新,区块链就瞎了。 一方面整条链交易速度会极慢,另一方面数据库容量极速膨胀,考虑到人手一份的存储机制,区块链其实是对存储资源和能源的一种极大的浪费。 这里还没有加上为了取得所谓的共识和挖矿消耗的巨大的能源,如果说区块链技术是屎,那么这波区块链投机浪潮可谓人类历史上最大规模的搅屎运动。 区块链也验证不了任何东西。 所谓的智能合约,即不智能,也非合约。我看有人还说,如果有了智能合约,就可以跟老板签一份放区块链上,如果明年销售业绩提升30%,就加薪10%,由于区块链不能篡改,不能抵赖,所以老板必须得执行,说得有板有眼,不懂行的愣一看,好像还真是那么回事。 但仔细一想,问题就来了。首先,在区块链上如何证明你真的达到了30%业绩提升?即便真的达到老板耍赖如何执行? 也就是说,如果区块链真这么厉害,要法院和仲裁干什么。 人类社会真正的符合成本效益原则的是代理制度。之前有人说要用区块链改造注册会计师行业,我不知道他准备怎么设计,我猜想他思路大概是这样的,首先肯定搞去中心化,让所有会计师到链上来,然后一个新人要成为注册会计师就要所有会计师同意并记录在链上。 那我就请问了,我每天上班累死累活,为什么还要花时间去验证一个跟我无关的的人的专业能力?最优做法当然是组织一个委员会,让专门的人来负责,这不就是现在注册会师协会干的事儿吗?区块链的逻辑相当于什么事情都要拿出来公投,这个绝对是扯淡的。 当然这么说都有点抬举区块链了,区块链技术本身根本没有判断是非能力,如果这么高级的人工智能,靠一个无脑分布式记账就能实现的话,我们早就进入共产主义社会了。 虽然EOS等数字货币采用了超级节点,通过再中心化的方式提高效率,有点行业协会的意思,是对区块链原教旨主义的一种修正,但是依然无法突破区块链技术最本质的局限性。有人说,私有链和联盟链是区块链技术的未来,也是扯淡,因为区块链技术没有未来。如果有,说明他是包装成区块链的伪区块链技术。 区块链所涉及的所有底层技术,不管是分布式数据库技术,加密技术,还是点对点传输技术等,基本都是早已存在没什么秘密可言的技术。 比特币系统最重要的特性是封闭性和自洽性,他验证不了任何系统自身以外产生的信息的真实性。 所谓系统自身产生的信息,就是数据库数据的变动信息,有价值的基本上有且只有交易信息。所以说比特币最初不过是中本聪一种炫技的产物,来证明自己对几种技术的掌握,你看我多牛逼,设计出了一个像三体一样的系统。因此,数字货币很有可能是区块链从始至终唯一的杀手应用。 比特币和区块链概念从诞生到今天已经快10年了,很多人说区块链技术在爆发的前夜,但这个前夜好像是不是有点过长了啊朋友,跟三体里的长夜有一拼啊。都说区块链技术像是90年代初的互联网,可是90年代初的互联网在十年发展后,已经出现了一大批伟大的公司,阿里巴巴在99年都成立了,区块链怎么除了币还是币呢? 正规的数字货币未来发展的形式无外乎几种,要么就是论坛币形式,或者类似股票的权益凭证等。问题是论坛币和股票之前,本来也都电子化了,区块链来了到底改变了什么呢? 所有想把TOKEN和应用场景结合起来的人最后都很痛苦,最后他们会发现区块链技术就是脱裤子放屁,自己辛苦搞半天,干嘛不自己作为中心关心门来收钱?最后这些人都产生了价值的虚无感,最终精神崩溃,只能发币疯狂收割韭菜,一边嘴里还说着我是个好人之类的奇怪的话。 因此,之前币圈链圈还泾渭分明,互相瞧不起,但这两年链圈逐渐坐不住了,想着是不是趁着泡沫没彻底破灭之前赶快收割一波,不然可能什么都捞不着了。 前段时间和一个名校毕业的链圈朋友瞎聊天,他说他们“致力于用区块链技术解决数字版权保护问题”,我就问他一个问题,你们如何保证你链的版权所有权声明是真实的,万一盗版者抢先一步把数据放在链上怎么办。他说他们的解决方案是连入国家数字版权保护中心的数据库进行验证…… 所以说区块链技术就是个鸡肋,研究到最后都会落入效率与真实性的黑洞,很多人一头扎进链圈后才发现,真正意义上的区块链技术,其实什么都干不了。 -02- 不是蠢就是坏的区块链媒体 空气币和区块链的造富神话,让区块链自媒体也开始迎风乱扭。一群群根本不知道区块链为何物的妖魔鬼怪纷纷进驻区块链自媒体战场,开始大放厥词胡编乱造。 任何东西,但凡只要和区块,链,分,分布式,记账,加密,验证,可追溯等等这些个关键词沾到哪怕一点点,这些所谓的区块链媒体人就会像狗闻到了屎了一样疯狂地把区块链概念往上套。 这让我想起曾经一度也是热闹非凡的物联网,我曾经去看过江苏一家号称要改变世界的“物联网”企业,过去一看是生产路由器的,我黑人问号脸,对方解释说没有路由器万物怎么互联,我觉得他说得好有道理,竟无言以对。 好,下面让我们进入奇葩共赏析时间,来看看区城链媒体经常有哪些危言耸听的奇谈怪论 区块链(分布式记账)的典型应用是*?? 正如前面所说,真正意义上的区块链分布式记账,不光包括“记”这个动作,还包括分布式存储和共识机制等。而*诞生远远早于区块链这个词的出现,勉强算是“分布式编辑”吧,就被很多区块链媒体拿来强行充当区块链技术应用的典范。 其实事实恰恰相反,*恰恰是去中心化失败的典范,现在如果没有精英和专业人士的编辑和维护,*早就没法看了。 区块链会促进社会分工?? 罗振宇好像就说过类似的话,虽然罗振宇说过很多没有逻辑的话,但这句话绝对是最没逻辑思维的。很多区块链自媒体也常常用这句话来忽悠老百姓,说分工代表效率提高社会进步,而区块链“无疑”会促进分工,他们的理由仅仅是分工和分布式记账都共用一个“分”字,就强行把他们扯到一起。 实际情况恰恰相反,区块链是逆分工的,区块链精神是号召所有人积极地参与到他不擅长也不想掺合的事情里面去。 区块链不能像上帝一样许诺他的子民死后上天国,只能给他们许诺你们是六度人脉中的第一级,我可以赚后面五级人的钱,你处于金字塔的顶端。
-
对话NGC蔡岩:从机制创新到价值沉淀,解析DeFi产品开发逻辑 |链捕手 - 真正的DeFi产品首先要有足够的安全性和稳定性,如果能在此基础上有一些功能创新,那就非常好了。像 Uniswap 这样逐渐成为 DeFi 基础架构的产品,可遇而不可求。 链式捕手:固定利率协议之前关注度比较高,但观察下来发现,大部分协议还是类似于传统金融CDO(抵押债务凭证)的玩法,风险系数很高,您如何理解这块业务的价值和风险? 蔡岩:确实有些定息协议类似CDO玩法,背后绑定一个债券,但并不是所有的定息协议都是这样的玩法,像这种CDO玩法的主要代表项目是88mph,背后绑定的是Aave、Compoud这样的借贷协议,在此基础上做定息和浮息债券;像APWine,背后同样是Aave,它会发行期货收益代币来锁定你的收益;Notional本身是做借贷市场的,在此基础上做定息协议。 非 CDO 的玩法,比如 Horizon,更像是一个利率撮合器,背后需要用户通过拍卖产生更合适的目标收益率;像 Saffron、BarnBridge 等是通过风险分级来定义不同的收益率。总的来说,创新还是挺多的。 价值层面是创新和想象力,因为在传统金融领域,比如银行做固定收益证券,或者评级机构给风险分级,这些业务都非常大,利润也很丰厚。而 DeFi 的对口业务给了类似业务很大的想象空间。尤其是固定利率协议的成熟产品不多,尝试各种微创新是很有意义的。 风险程度还是要具体到不同的玩法,比如,在 Aave、Compoud 等借贷协议的固定利率协议背后,如果这些借贷协议受到攻击,与之绑定的固定利率协议也会受损。 同样,如果自己做借贷市场,可能更需要更强的开发能力。再有,如果该程序的机制或参数设计不当,同样会导致协议运行不稳定,并可能造成大量用户清盘。 总的来说,风险在于固定利率协议的设计,这是一个非常复杂的过程,需要不断地尝试和出错。 链式捕捉器:刚刚提到背后是Aave/Compound的固定费率协议风险较大,您认为Aave最大的不确定性和创新点分在哪里? 蔡岩:其实爱钱进一直被认为是走在行业前列的项目,他们的迭代速度非常快,比如率先尝试闪贷、推出新的经济激励模式、推出目前业内首个安全模块、尝试L2解决方案等等。 而在主要的借贷业务上,他们又十分谨慎,比如在抵押率、清算系数等风险参数的设计上相对于其他借贷协议较为保守,并不会存在为了吸引更多借贷资金而降低风险的要求。 与许多 DeFi 项目一样,即使 Aave 进行了多次审计,也无法保证不存在漏洞。前段时间,Aave 刚进入 V2 阶段时,白帽黑客就指出了某个漏洞。 之前的创新点可能是闪电借贷,这是当时业内独一无二的新产品功能,也为 Aave 带来了不少收益。当然,也有人批评闪电贷只能方便黑客实现资金效益的最大化,但工具本身并没有错,未来闪电贷肯定会有更多的应用场景。 其次是安全模块的设计,这有点像项目本身的储备金库,保障项目的安全性,这也是爱维开创的先河。说实话,目前大多数项目都没有做到代币模式的良性或正向运营,也做不到像Aave一样的安全模块,这是一个不小的门槛。 Chaincatcher从某种程度上来说,挖矿模式是DeFi财富效应的根本支撑,但Aave的CEO却说挖矿机制带来的动力是不可持续的,您怎么看这个观点? 蔡岩:"挖矿机制 "不可能失效,因为它是一种激励机制,或者说是项目冷启动的一种方式。但流动性开采亚博体育手机客户端不会一直高涨。比如去年11月的流行性挖矿高APY持续了一两个月就崩盘了,导致DeFi市场大幅回调。 Aave、Uniswap、Synthetix等项目真正爆发进入市值前15名也是在今年2月,我更倾向于这是头部DeFi长期价值的体现。虽然大家都喜欢抢高APY的矿机,但我个人很少参与挖矿,所以我并不觉得流动性挖矿是DeFi的基本面支撑。
-
实时音频和视频技术的发展与应用-1.1 双重音频和视频 从架构上看,双人音视频系统相对简单明了。红点代表房间信令服务,房间信令服务的主要功能是管理房间信息,实现容量协商和上下行链路的质量调节,例如当下行信道发生拥塞时,上行线路的码率和分辨率会降低。 在传输信道层面,我们的策略是优先直连,在跨区域、跨运营商的情况下,我们会选择单中转或双中转信道,在策略上尽量保持直连和中转信道同时存在,当其中一个信道的质量不好时,系统会自动切断到另一个信道的流量。 1.2 多人音视频 多人视频通话的产品形态是整个房间不超过 50 人,大盘平均房间规模约为 4.x 人,房间内部最多满足一个大视频和三个小视频(四屏)。根据这一条件,我们在架构中采用了典型的 SFU 小房间设计。 上图中的红点代表房间信令服务,主要用于房间管理和状态信息同步。房间管理主要包括用户列表的管理,例如哪些用户打开了视频/音频,我看了谁,谁看了我,这些都是基于房间管理的信息,然后房间信令服务会将这些信息同步到媒体传输服务进行数据分发。 房间服务的另一个作用是房间级容量协商和质量控制,例如,房间里的每个人一开始都支持 H.265 编码,当某个时刻进来一个只支持 H.264 编码的用户时,房间里所有的上游主播就必须把 H.265 切成 H.264。还有一种情况是,房间里有一定比例的人下行链路信道质量较差,这会导致上行链路房间质量下降。 在传输层面,我们采用的是单层分布式媒体传输网络,大家都选择中转方式,不区分双人和多人,采用 Full-Mesh 传输机制将所有数据推送过去,比如一个节点上的人并不都看另外两个人的视频,但还是会将视频推送给他们。
-
Redis 中的缓存设计 - 缓存与数据库双倍写入不一致
-
Redis 设计与实现说明 (XXXIII) - Redis 排序命令 sort 的实现