Redis运行比较快主要原因有以下几种:
Redis版本在6.0之前都是使用的单线程运行的。所有的客户端的请求处理、命令执行以及数据读写操作都是在一个主线程中完成得。这种设计目的就是为了防止多线程环境下的锁竞争和上下文切换所带来的性能开销,这样保证在高并发场景下的性能
Redis版本在6.0中,开始引入了多线程的支持,但是这仅限于网络I/O层面,即在网络请求阶段使用工作线程进行处理,对于指令的执行过程,仍然是在主线程来处理,所以不会存在多个线程通知执行操作指令的情况
关于线程安全问题,从Redis服务层面俩看,Redis Server本身就是一个线程安全按的K-V数据库,也就是说在Redis Server上面执行的指令,不需要任何同步机制,不会存在线程安全问题
Redis在实际工作中广泛应用于多种业务场景,以下是一些常见的例子:
缓存:Redis作为Key-Value形态的内存数据库,最先会被想到的应用场景就是作为数据缓存。Redis提供了键过期功能,也提供了键淘汰策略,所以Redis用在缓存的场合非常多
排行榜:很多网站都有排行榜应用,如京东的月度销量榜单、商品按时间上新排行榜等。Redis提供的有序集合(zset)数据类
分布式会话:在集群模式下,一般会搭建以Redis等内存等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理
分布式锁:在高并发的情景,可以利用Redis的setnx功能来编写分布式锁
Redis中,常见的数据类型有如下几种:
在实际工作中,小型的项目会使用Redis存储Session信息,但是不同业务场景存储Session信息的类型也是不同的,具体来说:
存储数据简单(不涉及局部更新):使用String类型粗怒触Session,这样做的优缺点如下:
优点:
? ? ? ? 存取操作简单直观,只需要单个键执行操作即可
? ? ? ? 多余小型Session,存储开销相对于较小
缺点:
? ? ? ? 如果Session数据复杂或者需要频繁更新其中的部分字段,则每次更新都需要重新序列化整个Session对象
? ? ? ? 不利于查询Session内特定字段值
存储数据复杂(涉及局部更新):如果Session数据结构复杂且需要频繁更新或查询其中个别字段,通常建议使用哈希表存储Session。每个Session视为一个独立的哈希表,Session ID作为key,Sesion内各个字段作为field-value对存储在哈希表中。示例:HSET session:123 userId 123 username user1,这样做的优缺点如下:
优点:
????????可以方便地进行字段级别的读写操作,例如 HGET session:23 userd 和 HSET session:123 lastAccessTime now
????????更新部分字段时无需修改整个Session内容
缺点:
? ? ? ? 相对于简单的字符串存储,哈希表占用的空间可能更大,尤其时当Session数据包含多个值的时候
小结: 如果 Session 数据结构复杂且需要频繁更新或查询其中的个别字段,通常建议使用哈希表来存储 Session;而在 Session 数据较为简单、不涉及局部更新的情况下,使用字符串存储也是可行的选择
在Redis7之前,有序集合使用的是ziplist(压缩列表)+skiplist(跳跃表),当数据列表元素小于128个,并且所有元素成员的长度都小于64字节时,使用压缩列表存储,否则使用调表存储
在Redis之后,有序集合使用listPack(紧凑列表)+skiplist(跳跃表)
skiplist是一种以空间换时间的数据结构。由于链表无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中的关键姐点(索引),现在关键节点上查找,在进入下层链表查找提取多层关键节点,就形成了跳表。但是由于索引要占据一定的空间,所以索引添加的越多,占用的空间越多。
对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高O(N)
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。时间复杂度从原来的O(n)到O(logn),是一空间换时间的解决方法
跳表的查询流程如下:
跳表的添加流程主要是包括以下步骤:
查找插入位置:首先,我们需要找到新元素应该插入的位置。这个过程与跳表查找操作类似,我们从最高层所以一年开始,逐层向下查找直到找到最后一个位置,使得该位置前面的元素小于新元素,后面的元素大于新元素
生成随机层数:在确定新元素插入位置后,我们需要决定新元素在跳表中的层数。这个层数是通过一个随机函数生成的。每个节点肯定都有第一层指针(每个节点都在第一层链表中)。如果一个节点有第i层指针(即节点已经在第一层到第i层链表中),那么他又第(i+1)层指针的
插入新元素:根据生成的随机层数,我们在相应的层中插入新元素。对于每一层,我们都需要更新相应的前驱和后继指针,使得它们指向新插入的元素
更新跳表的最大层数:如果新插入的元素的层数大于跳表的当前最大层数,我们需要更新跳表的最大层数
关于“随机层数”的概念,其主要目的是为了保持跳表的平衡性。如果我们固定每个元素的层数,那么在某些情况下,跳表可能会退化成普通的链表,从而导致查找效率降低。通过随机选择每个元素的层数,我们可以确保跳表的高度大致为log(n),从而保证查找、插入和删除操作的时间复杂度为O(log n)
给定如上跳表,假设要插入节点2。首先需要判断节点2是否已经存在,若存在则返回false
。否则,随机生成待插入节点的层数
/**
* 生成随机层数[0,maxLevel)
* 生成的值越大,概率越小
*
* @return
*/
private int randomLevel() {
int level = 0;
while (Math.random() < PROBABILITY && level < maxLevel - 1) {
++level;
}
return level;
}
这里的PROBABILITY =0.5。上面算法的意思是返回1的概率是1/2,返回2的概率是1/4,返回3的概率是1/8,依次类推。看成一个分布的话,第0层包含所有节点,第1层含有1/2个节点,第2层含有1/4?个节点…
注意这里有一个最大层数maxLevel ,也可以不设置最大层数。通过这种随机生成层数的方式使得实现起来简单。假设我们生成的层数是3
在1和3之间插入节点2,层数是3,也就是节点2跳跃到了第3层
public boolean add(E e) {
if (contains(e)) {
return false;
}
int level = randomLevel();
if (level > curLevel) {
curLevel = level;
}
Node newNode = new Node(e);
Node current = head;
//插入方向由上到下
while (level >= 0) {
//找到比e小的最大节点
current = findNext(e, current, level);
//将newNode插入到current后面
//newNode的next指针指向该节点的后继
newNode.forwards.add(0, current.next(level));
//该节点的next指向newNode
current.forwards.set(level, newNode);
level--;//每层都要插入
}
size++;
return true;
}
我们通过一个例子来模拟,由于实现了直观的打印算法。假设我们要插入1, 6, 9, 3, 5, 7, 4, 8
?过程如下:
add: 1
Level 0: 1
add: 6
Level 0: 1 6
add: 9
Level 2: 9
Level 1: 9
Level 0: 1 6 9
add: 3
Level 2: 3 9
Level 1: 3 9
Level 0: 1 3 6 9
add: 5
Level 2: 3 9
Level 1: 3 5 9
Level 0: 1 3 5 6 9
add: 7
Level 2: 3 9
Level 1: 3 5 9
Level 0: 1 3 5 6 7 9
add: 4
Level 2: 3 9
Level 1: 3 5 9
Level 0: 1 3 4 5 6 7 9
add: 8
Level 2: 3 9
Level 1: 3 5 9
Level 0: 1 3 4 5 6 7 8 9
实现分布式锁
使用Redis实现分布式锁可以通过setnx(set if not exists)命令实现,但当我们使用setnx创建键值成功时,则表中加锁成功,否则代码加锁失败,实现示例如下:
127.0.0.1:6379> setnx lock true
(integer) 1#创建锁成功
#逻辑业务处理..
当我们重复加锁时,只有第一次会加锁成功
127.8..1:6379> setnx lock true # 第一次加锁
(integer) 1
127.8.8.1:6379> setnx lock true # 第二次加锁
(integer) 0
从上述命令可以看出,我们可以看执行结果返回是不是1,就可以看出是否加锁成功
释放分布式锁
127.0.0.1:6379> de1 lock
(integer) 1 #释放锁
然而,如果使用 setnx ock true 实现分布式锁会存在死锁问题,以为 setnx 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况
解决死锁问题
死锁问题可以通过设置超时时间来解决,如果超过了超时时间,分布锁会自动释放,这样就不会存在死锁问题了也就是 setnx和 expire 配合使用,在 Redis 2.6.12 版本之后,新增了一个强大的功能,我们可以使用一个原子操作也就是一条命令来执行 setnx 和expire 操作了,实现示例如下:
127.0.0.1:6379> set lock true ex 3 nx
OK #创建锁成功
127...1:6379> set lock true ex 3 nx
(ni1) #在锁被占用的时候,企图获取锁失败
其中ex为设置超时时间, nx 为元素非空判断,用来判断是否能正常使用锁的。
因此,我们在 Redis 中实现分布式锁最直接的方案就是使用 set key value ex timeout nx 的方式来实现。