hgetall hash-key
-
“sub-key1”
-
“value1”
-
“sub-key2”
-
“value2”
hdel hash-key sub-key2
(integer) 1
hdel hash-key sub-key2
(integer) 0
hget hash-key sub-key1
“value1”
hgetall hash-key
-
“sub-key1”
-
“value1”
1.3.4SET
常见操作
sadd set-key item
(integer) 1
sadd set-key item2
(integer) 1
sadd set-key item3
(integer) 1
sadd set-key item
(integer) 0
smembers set-key
-
“item2”
-
“item”
-
“item3”
sismember set-key item4
(integer) 0
sismember set-key item
(integer) 1
srem set-key item
(integer) 1
srem set-key item
(integer) 0
smembers set-key
-
“item2”
-
“item3”
应用场景
Redis为集合提供了求交集、并集、差集等操作,故可以用来求共同好友等操作。
1.3.5ZSET
数据结构
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLever{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}lever[];
}
typedef struct zskiplist{
//表头节点跟表尾结点
struct zskiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int lever;
}
跳跃表,基于多指针有序链实现,可以看作多个有序链表。
与红黑树等平衡树相比,跳跃表具有以下优点:
-
插入速度非常快速,因为不需要进行旋转等操作来维持平衡性。
-
更容易实现。
-
支持无锁操作。
常见操作
zadd zset-key 728 member1
(integer) 1
zadd zset-key 982 member0
(integer) 1
zadd zset-key 982 member0
(integer) 0
zrange zset-key 0 -1
-
“member1”
-
“member0”
zrange zset-key 0 -1 withscores
-
“member1”
-
“728”
-
“member0”
-
“982”
zrangebyscore zset-key 0 800 withscores
-
“member1”
-
“728”
zrem zset-key member1
(integer) 1
zrem zset-key member1
(integer) 0
zrange zset-key 0 -1 withscores
-
“member0”
-
“982”
应用场景
以某个条件为权重,比如按顶的次数排序
ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
1.4Spring整合Redis
引入依赖
- spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-data-redis
配置Redis
- 配置数据库参数
RedisProperties
spring.redis.database=11#第11个库,这个随便
spring.redis.host=localhost
spring.redis.port=6379#端口
- 编写配置类,构造RedisTemplate
这个springboot已经帮我们配了,但是默认object,我想改成string
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
访问Redis
- redisTemplate.opsForValue()
- redisTemplate.opsForHash()
- redisTemplate.opsForList()
- redisTemplate.opsForSet()
- redisTemplate.opsForZSet()
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testStrings() {
String redisKey = “test:count”;
redisTemplate.opsForValue().set(redisKey, 1);
System.out.println(redisTemplate.opsForValue().get(redisKey));
System.out.println(redisTemplate.opsForValue().increment(redisKey));
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}
@Test
public void testHashes() {
String redisKey = “test:user”;
redisTemplate.opsForHash().put(redisKey, “id”, 1);
redisTemplate.opsForHash().put(redisKey, “username”, “zhangsan”);
System.out.println(redisTemplate.opsForHash().get(redisKey, “id”));
System.out.println(redisTemplate.opsForHash().get(redisKey, “username”));
}
@Test
public void testLists() {
String redisKey = “test:ids”;
redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);
System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println(redisTemplate.opsForList().index(redisKey, 0));
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}
@Test
public void testSets() {
String redisKey = “test:teachers”;
redisTemplate.opsForSet().add(redisKey, “刘备”, “关羽”, “张飞”, “赵云”, “诸葛亮”);
System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
@Test
public void testSortedSets() {
String redisKey = “test:students”;
redisTemplate.opsForZSet().add(redisKey, “唐僧”, 80);
redisTemplate.opsForZSet().add(redisKey, “悟空”, 90);
redisTemplate.opsForZSet().add(redisKey, “八戒”, 50);
redisTemplate.opsForZSet().add(redisKey, “沙僧”, 70);
redisTemplate.opsForZSet().add(redisKey, “白龙马”, 60);
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, “八戒”));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, “八戒”));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
@Test
public void testKeys() {
redisTemplate.delete(“test:user”);
System.out.println(redisTemplate.hasKey(“test:user”));
redisTemplate.expire(“test:students”, 10, TimeUnit.SECONDS);
}
}
这样还是稍微有点麻烦,我们其实可以绑定key
// 多次访问同一个key
@Test
public void testBoundOperations() {
String redisKey = “test:count”;
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
operations.increment();
operations.increment();
operations.increment();
operations.increment();
System.out.println(operations.get());
}
======================================================================
这部分在我看来是最有意思的,我们有必要了解底层数据结构的实现,这也是我最感兴趣的。
比如,你知道redis中的字符串怎么实现的吗?为什么这么实现?
你知道redis压缩列表是什么算法吗?
你知道redis为什么抛弃了红黑树反而采用了跳表这种新的数据结构吗?
你知道hyperloglog为什么用如此小的空间就可以有这么好的统计性能和准确性吗?
你知道布隆过滤器为什么这么有效吗?有没有数学证明过?
你是否还能很快写出来快排?或者不断优化性能的排序?是不是只会调库了甚至库函数怎么实现的都不知道?真的就是快排?
包括数据库,持久化,处理事件、客户端服务端、事务的实现、发布和订阅等功能的实现,也需要了解。
2.1数据结构和对象的实现
- 1) 字符串
redis并未使用传统的c语言字符串表示,它自己构建了一种简单的动态字符串抽象类型。
在redis里,c语言字符串只会作为字符串字面量出现,用在无需修改的地方。
当需要一个可以被修改的字符串时,redis就会使用自己实现的SDS(simple dynamic string)。比如在redis数据库里,包含字符串的键值对底层都是SDS实现的,不止如此,SDS还被用作缓冲区(buffer):比如AOF模块中的AOF缓冲区以及客户端状态中的输入缓冲区。
下面来具体看一下sds的实现:
struct sdshdr
{
int len;//buf已使用字节数量(保存的字符串长度)
int free;//未使用的字节数量
char buf[];//用来保存字符串的字节数组
};
sds遵循c中字符串以’\0’结尾的惯例,这一字节的空间不算在len之内。
这样的好处是,我们可以直接重用c中的一部分函数。比如printf;
sds相对c的改进
获取长度:c字符串并不记录自身长度,所以获取长度只能遍历一遍字符串,redis直接读取len即可。
缓冲区安全:c字符串容易造成缓冲区溢出,比如:程序员没有分配足够的空间就执行拼接操作。而redis会先检查sds的空间是否满足所需要求,如果不满足会自动扩充。
内存分配:由于c不记录字符串长度,对于包含了n个字符的字符串,底层总是一个长度n+1的数组,每一次长度变化,总是要对这个数组进行一次内存重新分配的操作。因为内存分配涉及复杂算法并且可能需要执行系统调用,所以它通常是比较耗时的操作。
redis内存分配:
1、空间预分配:如果修改后大小小于1MB,程序分配和len大小一样的未使用空间,如果修改后大于1MB,程序分配 1MB的未使用空间。修改长度时检查,够的话就直接使用未使用空间,不用再分配。
2、惰性空间释放:字符串缩短时不需要释放空间,用free记录即可,留作以后使用。
二进制安全
c字符串除了末尾外,不能包含空字符,否则程序读到空字符会误以为是结尾,这就限制了c字符串只能保存文本,二进制文件就不能保存了。
而redis字符串都是二进制安全的,因为有len来记录长度。
- 2) 链表
作为一种常用数据结构,链表内置在很多高级语言中,因为c并没有,所以redis实现了自己的链表。
链表在redis也有一定的应用,比如列表键的底层实现之一就是链表。(当列表键包含大量元素或者元素都是很长的字符串时)
发布与订阅、慢查询、监视器等功能也用到了链表。
具体实现:
//redis的节点使用了双向链表结构
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
//其实学过数据结构的应该都实现过
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;
总结一下redis链表特性:
双端、无环、带长度记录、
多态:使用 void*
指针来保存节点值, 可以通过 dup
、 free
、 match
为节点值设置类型特定函数, 可以保存不同类型的值。
- 3)字典
其实字典这种数据结构也内置在很多高级语言中,但是c语言没有,所以redis自己实现了。
应用也比较广泛,比如redis的数据库就是字典实现的。不仅如此,当一个哈希键包含的键值对比较多,或者都是很长的字符串,redis就会用字典作为哈希键的底层实现。
来看看具体是实现:
//redis的字典使用哈希表作为底层实现
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
table
是一个数组, 数组中的每个元素都是一个指向dictEntry
结构的指针, 每个 dictEntry
结构保存着一个键值对。
图为一个大小为4的空哈希表。
我们接着就来看dictEntry的实现:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
(v可以是一个指针, 或者是一个 uint64_t
整数, 又或者是一个 int64_t
整数。)
next就是解决键冲突问题的,冲突了就挂后面,这个学过数据结构的应该都知道吧,不说了。
下面我们来说字典是怎么实现的了。
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
int rehashidx; //* rehashing not in progress if rehashidx == -1
} dict;
type
和 privdata
是对不同类型的键值对, 为创建多态字典而设置的:
type
指向 dictType
, 每个 dictType
保存了用于操作特定类型键值对的函数, 可以为用途不同的字典设置不同的类型特定函数。
而 privdata
属性则保存了需要传给那些类型特定函数的可选参数。
而dictType就暂时不展示了,不重要而且字有点多。。。还是讲有意思的东西吧
rehash(重新散列)
随着我们不断的操作,哈希表保存的键值可能会增多或者减少,为了让哈希表的负载因子维持在合理的范围内,有时需要对哈希表进行合理的扩展或者收缩。 一般情况下, 字典只使用 ht[0]
哈希表, ht[1]
哈希表只会在对 ht[0]
哈希表进行 rehash 时使用。
redis字典哈希rehash的步骤如下:
1)为ht[1]分配合理空间:如果是扩展操作,大小为第一个大于等于ht[0]*used*2的,2的n次幂。
如果是收缩操作,大小为第一个大于等于ht[0]*used的,2的n次幂。
2)将ht[0]中的数据rehash到ht[1]上。
3)释放ht[0],将ht[1]设置为ht[0],ht[1]创建空表,为下次做准备。
渐进rehash
数据量特别大时,rehash可能对服务器造成影响。为了避免,服务器不是一次性rehash的,而是分多次。
我们维持一个变量rehashidx,设置为0,代表rehash开始,然后开始rehash,在这期间,每个对字典的操作,程序都会把索引rehashidx上的数据移动到ht[1]。
随着操作不断执行,最终我们会完成rehash,设置rehashidx为-1.
需要注意:rehash过程中,每一次增删改查也是在两个表进行的。
- 4)整数集合
整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 可以保存 int16_t
、 int32_t
、 int64_t
的整数值, 并且保证集合中不会出现重复元素。
实现较为简单:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
各个项在数组中从小到大有序地排列, 并且数组中不包含任何重复项。
虽然 intset
结构将 contents
属性声明为 int8_t
类型的数组, 但实际上 contents
数组并不保存任何 int8_t
类型的值 —— contents
数组的真正类型取决于 encoding
属性的值:
如果 encoding
属性的值为 INTSET_ENC_INT16
, 那么 contents
就是一个 int16_t
类型的数组, 数组里的每个项都是一个 int16_t
类型的整数值 (最小值为 -32,768
,最大值为 32,767
)。
如果 encoding
属性的值为 INTSET_ENC_INT32
, 那么 contents
就是一个 int32_t
类型的数组, 数组里的每个项都是一个 int32_t
类型的整数值 (最小值为 -2,147,483,648
,最大值为 2,147,483,647
)。
如果 encoding
属性的值为 INTSET_ENC_INT64
, 那么 contents
就是一个 int64_t
类型的数组, 数组里的每个项都是一个 int64_t
类型的整数值 (最小值为 -9,223,372,036,854,775,808
,最大值为 9,223,372,036,854,775,807
)。
升级
c语言是静态类型语言,不允许不同类型保存在一个数组。这样第一,灵活性较差,第二,有时会用掉不必要的内存
比如用long long储存1
为了提高整数集合的灵活性和节约内存,我们引入升级策略。
当我们要将一个新元素添加到集合里, 并且新元素类型比集合现有元素的类型都要长时, 集合需要先进行升级。
分为三步进行:
-
根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
-
将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上
-
将新元素添加到底层数组里面。
因为每次添加新元素都可能会引起升级, 每次升级都要对已有元素类型转换, 所以添加新元素的时间复杂度为 O(N) 。
因为引发升级的新元素比原数据都长,所以要么他是最大的,要么他是最小的。我们把它放在开头或结尾即可。
降级
略略略,不管你们信不信,整数集合不支持降级操作。。我也不知道为啥
- 5)压缩列表
压缩列表是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项,并且列表项都是小整数或者短字符串,redis就会用压缩列表做列表键底层实现。
压缩列表是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
一个压缩列表可以包含任意多个节点(entry), 每个节点可以保存一个字节数组或者一个整数值。
具体实现:
具体说一下entry:
由三个部分组成:
1、previous_entry_length:记录上一个节点的长度,这样我们就可以从最后一路遍历到开头。
2、encoding:记录了content所保存的数据类型和长度。(具体编码不写了,不重要)
3、content:保存节点值,可以是字节数组或整数。(具体怎么压缩的等我搞明白再补)
连锁更新
前面说过, 每个节点的 previous_entry_length
属性都记录了前一个节点的长度:
-
如果前一节点的长度<
254
KB, 那么previous_entry_length
需要用1
字节长的空间 -
如果前一节点的长度>=
254
KB, 那么previous_entry_length
需要用5
字节长的空间
现在, 考虑这样一种情况: 在一个压缩列表中, 有多个连续的、长度介于 250
字节到 253
字节之间的节点 ,这时, 如果我们将一个长度大于等于 254
字节的新节点 new
设置为压缩列表的表头节点。。。。
然后脑补一下,就会导致连锁扩大每个节点的空间对吧?e(i)因为e(i-1)的扩大而扩大,i+1也是如此,以此类推。。。
删除节点同样会导致连锁更新。
这个事情只是想说明一个问题:插入删除操作的最坏时间复杂度其实是o(n*n),因为每更新一个节点都要o(n)。
但是,也不用太过担心,因为这种特殊情况并不多见,这些命令的平均复杂度依旧是o(n)。
2.2 跳表专栏
2.2.1跳表是啥
为什么选择了跳表而不是红黑树?
跳表是个啥东西请看这个文章。
我们知道,节点插入时随机出一个层数,仅仅依靠一个简单的随机数操作而构建出来的多层链表结构,能保证它有一个良好的查找性能吗?为了回答这个疑问,我们需要分析skiplist的统计性能。
在分析之前,我们还需要着重指出的是,执行插入操作时计算随机数的过程,是一个很关键的过程,它对skiplist的统计特性有着很重要的影响。这并不是一个普通的服从均匀分布的随机数,它的计算过程如下:
-
首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
-
如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。
-
节点最大的层数不允许超过一个最大值,记为MaxLevel。
这个计算随机层数的伪码如下所示:
randomLevel()
level := 1
// random()返回一个[0…1)的随机数
while random() < p and level < MaxLevel do
level := level + 1
return level
randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为:
p = 1/4
MaxLevel = 32
2.2.2skiplist的算法性能分析
在这一部分,我们来简单分析一下skiplist的时间复杂度和空间复杂度,以便对于skiplist的性能有一个直观的了解。如果你不是特别偏执于算法的性能分析,那么可以暂时跳过这一小节的内容。
我们先来计算一下每个节点所包含的平均指针数目(概率期望)。节点包含的指针数目,相当于这个算法在空间上的额外开销(overhead),可以用来度量空间复杂度。
根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:
-
节点层数至少为1。而大于1的节点层数,满足一个概率分布。
-
节点层数恰好等于1的概率为1-p。
-
节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
-
节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2(1-p)。
-
节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3(1-p)。
-
…
因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
现在很容易计算出:
-
当p=1/2时,每个节点所包含的平均指针数目为2;
-
当p=1/4时,每个节点所包含的平均指针数目为1.33。这也是Redis里的skiplist实现在空间上的开销。
接下来,为了分析时间复杂度,我们计算一下skiplist的平均查找长度。查找长度指的是查找路径上跨越的跳数,而查找过程中的比较次数就等于查找长度加1。以前面图中标出的查找23的查找路径为例,从左上角的头结点开始,一直到结点22,查找长度为6。
为了计算查找长度,这里我们需要利用一点小技巧。我们注意到,每个节点插入的时候,它的层数是由随机函数randomLevel()计算出来的,而且随机的计算不依赖于其它节点,每次插入过程都是完全独立的。所以,从统计上来说,一个skiplist结构的形成与节点的插入顺序无关。
这样的话,为了计算查找长度,我们可以将查找过程倒过来看,从右下方第1层上最后到达的那个节点开始,沿着查找路径向左向上回溯,类似于爬楼梯的过程。我们假设当回溯到某个节点的时候,它才被插入,这虽然相当于改变了节点的插入顺序,但从统计上不影响整个skiplist的形成结构。
现在假设我们从一个层数为i的节点x出发,需要向左向上攀爬k层。这时我们有两种可能:
-
如果节点x有第(i+1)层指针,那么我们需要向上走。这种情况概率为p。
-
如果节点x没有第(i+1)层指针,那么我们需要向左走。这种情况概率为(1-p)。
用C(k)表示向上攀爬k个层级所需要走过的平均查找路径长度(概率期望),那么:
C(0)=0
C(k)=(1-p)×(上图中情况b的查找长度) + p×(上图中情况c的查找长度)
代入,得到一个差分方程并化简:
C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
C(k)=1/p+C(k-1)
C(k)=k/p
这个结果的意思是,我们每爬升1个层级,需要在查找路径上走1/p步。而我们总共需要攀爬的层级数等于整个skiplist的总层数-1。
那么接下来我们需要分析一下当skiplist中有n个节点的时候,它的总层数的概率均值是多少。这个问题直观上比较好理解。根据节点的层数随机算法,容易得出:
-
第1层链表固定有n个节点;
-
第2层链表平均有n*p个节点;
-
第3层链表平均有n*p^2个节点;
-
…
所以,从第1层到最高层,各层链表的平均节点数是一个指数递减的等比数列。容易推算出,总层数的均值为log1/pn,而最高层的平均节点数为1/p。
综上,粗略来计算的话,平均查找长度约等于:
- C(log1/pn-1)=(log1/pn-1)/p
即,平均时间复杂度为O(log n)。
当然,这里的时间复杂度分析还是比较粗略的。比如,沿着查找路径向左向上回溯的时候,可能先到达左侧头结点,然后沿头结点一路向上;还可能先到达最高层的节点,然后沿着最高层链表一路向左。但这些细节不影响平均时间复杂度的最后结果。另外,这里给出的时间复杂度只是一个概率平均值,但实际上计算一个精细的概率分布也是有可能的。
详情还请参见William Pugh的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。
2.2.3skiplist与平衡树、哈希表的比较
-
skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
-
在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
-
平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
-
从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
-
查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
-
从算法实现难度上来比较,skiplist比平衡树要简单得多。
2.2.4Redis中的skiplist和经典有何不同
-
分数(score)允许重复,即skiplist的key允许重复。这在最开始介绍的经典skiplist中是不允许的。
-
在比较时,不仅比较分数(相当于skiplist的key),还比较数据本身。在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。
-
第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。
-
在skiplist中可以很方便地计算出每个元素的排名(rank)。
2.2.5作者的话
最后我们看看,对于这个问题,Redis的作者 @antirez 是怎么说的:
There are a few reasons:
-
They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
-
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
-
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
有几个原因:
1)它们的记忆力不是很强。基本上由你决定。更改有关节点具有给定数量级别的概率的参数将使内存密集度低于btree。
2)排序集通常是许多Zrange或Zrevrange操作的目标,即作为链表遍历跳过列表。通过此操作,跳过列表的缓存区域性至少与其他类型的平衡树一样好。
3)它们易于实现、调试等。例如,由于跳过列表的简单性,我收到了一个补丁(已经在redis master中),其中包含在o(log(n))中实现zrank的扩展跳过列表。它只需要对代码稍作修改。
2.3HyperLogLog 专栏
HyperLogLog 是一种概率数据结构,用来估算数据的基数。数据集可以是网站访客的 IP 地址,E-mail 邮箱或者用户 ID。
基数就是指一个集合中不同值的数目,比如 a, b, c, d 的基数就是 4,a, b, c, d, a 的基数还是 4。虽然 a 出现两次,只会被计算一次。
使用 Redis 统计集合的基数一般有三种方法,分别是使用 Redis 的 HashMap,BitMap 和 HyperLogLog。前两个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。
Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。
这是一个很惊人的结果,以如此小的内存来记录如此大数量级的数据基数。下面我们就带大家来深入了解一下 HyperLogLog 的使用,基础原理,源码实现和具体的试验数据分析。
2.3.1HyperLogLog 在 Redis 中的使用
Redis 提供了 PFADD
、 PFCOUNT
和 PFMERGE
三个命令来供用户使用 HyperLogLog。
PFADD
用于向 HyperLogLog 添加元素。
PFADD visitors alice bob carol
(integer) 1
PFCOUNT visitors
(integer) 3
如果 HyperLogLog 估计的近似基数在 PFADD
命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0 。 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令。
PFCOUNT
命令会给出 HyperLogLog 包含的近似基数。在计算出基数后, PFCOUNT
会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD
执行成功前,就都不需要再次进行基数的计算。
PFMERGE
将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。
PFADD customers alice dan
(integer) 1
PFMERGE everyone visitors customers
OK
PFCOUNT everyone
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
然 a 出现两次,只会被计算一次。
使用 Redis 统计集合的基数一般有三种方法,分别是使用 Redis 的 HashMap,BitMap 和 HyperLogLog。前两个数据结构在集合的数量级增长时,所消耗的内存会大大增加,但是 HyperLogLog 则不会。
Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要不不高的场景。
这是一个很惊人的结果,以如此小的内存来记录如此大数量级的数据基数。下面我们就带大家来深入了解一下 HyperLogLog 的使用,基础原理,源码实现和具体的试验数据分析。
2.3.1HyperLogLog 在 Redis 中的使用
Redis 提供了 PFADD
、 PFCOUNT
和 PFMERGE
三个命令来供用户使用 HyperLogLog。
PFADD
用于向 HyperLogLog 添加元素。
PFADD visitors alice bob carol
(integer) 1
PFCOUNT visitors
(integer) 3
如果 HyperLogLog 估计的近似基数在 PFADD
命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0 。 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令。
PFCOUNT
命令会给出 HyperLogLog 包含的近似基数。在计算出基数后, PFCOUNT
会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD
执行成功前,就都不需要再次进行基数的计算。
PFMERGE
将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。
PFADD customers alice dan
(integer) 1
PFMERGE everyone visitors customers
OK
PFCOUNT everyone
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-mCOo5k0o-1710847610786)]
[外链图片转存中…(img-PanSZhtG-1710847610787)]
[外链图片转存中…(img-ijeRLltA-1710847610787)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-LCS1W0MW-1710847610787)]