redis
redis的几种java客户端
- jedis:
- Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
- Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。
- redisson:
- 基于Netty实现,采用非阻塞IO,性能高,对分布式支持更好。
- Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
- 功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性
- Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。
- 封装了基本java中的所有数据类型
springDataJedit
https://blog.csdn.net/lydms/article/details/105224210
springDataJedis是spring对jedis又一层封装。
springDataJedis性能低于jedis。
穿透、击穿、雪崩
雪崩
请求高峰期,如果缓存挂了(redi宕机或者数据大量过期),所有请求打到数据库,数据库承受不住被打挂,dba重启数据库继续被请求打挂。
如何处理:
- 保证redis高可用
- 增加二级缓存一起来分担压力
- 限流
击穿和穿透
- 穿透:大量key的请求都是缓存和数据库中都不存在,这些请求都打到数据库
- 查询数据库没查询到数据的话也更新redis,设置一个空值,则后续请求会命中redis。
- bloomFilter:将所有缓存的key都放入到bloomFilter中,请求过来先查看bloomFilter中key是否存在,不存在直接返回,存在在走缓存-数据库。
bloomFilter在guvaa包下,使用api类似map。优点是支持大数据量,缺点是单机、存在一定误差。
- 击穿:大量相同key的请求同时过来,正好此时缓存中该key过期,则大量请求都打到数据库。
- 这个问题一般在多线程查询的场景,在线程上加一个锁,如果缓存中查询不到数据则锁住线程,其他线程获取不到锁等待,直到刚才的线程从db中查询到数据并且放入缓存释放锁。
bloomFilter
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性
- 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。
- 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter
事务
redis事务中所有命令是个原子操作。
multi开启事务,exec提交事务。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd "user:1:following" 2
QUEUED
127.0.0.1:6379> sadd "user:2:followers" 1
QUEUED
127.0.0.1:6379> exec
上面的代码演示了事务的使用方式。首先使用multi命令告诉redis下面的命令属于同一个事务。
redis会把下面的两个sadd命令暂时存起来,返回QUEUED表示这两条命令已经进入等待执行的事务队列中了。
当把所有要在一个事务中执行的命令都发给redis后,我们使用exec命令告诉redis将等待执行的事务队列中的所有命令,即前面所有返回QUEUED的命令,按照发送顺序依次执行。
exec命令的返回值就是这些命令返回值组成的列表,返回值顺序和命令的顺序相同。
所以当exec提交后,事务中如果某条命令执行失败,其他命令也无法回滚
事务可以和watch命令一起使用
事务都是在有竞态的时候才会使用,一般都是在开启事务的时候查出一个多线程共享的资源,然后提交的时候检查一下该资源是否被其他线程更改过,如果改过则需要放弃执行。
watch key
multi
命令1
命令2
exac
如果在exac执行之前key被其他线程修改了,则该事务就不会执行。
unwatch释放监控。
管道
当需要向服务器提交多条命令的时候,如果一条一条提交,效率不友好,可以使用管道批量提交。
@Test
public void testNoPipelineSet() {
Jedis jedis = jedisPool.getResource();
String keyPrefix = "normal";
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
String value = String.valueOf(i);
jedis.set(key, value);
}
jedis.close();
long end = System.currentTimeMillis();
System.out.println("not use pipeline batch set total time:" + (end - begin));
}
@Test
public void testPipelineSet() {
Jedis jedis = jedisPool.getResource();
Pipeline pipelined = jedis.pipelined();
String keyPrefix = "pipeline";
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
String value = String.valueOf(i);
pipelined.set(key, value);
}
pipelined.sync();
jedis.close();
long end = System.currentTimeMillis();
System.out.println("use pipeline batch set total time:" + (end - begin));
}
not use pipeline batch set total time:998
use pipeline batch set total time:31
可以看到使用管道效率明显提高
脚本
脚本中的操作都是原子操作。
脚本可以提前存储到服务端,也可以通过redis-cli命令发送到服务端(后续会一直存储在服务端)。
redis-cli中可以直接通过eval函数表示发送lua脚本。
事务、管道、脚本比较
事务本质上还是一条一条的向服务端发送命令。
在执行多条命令的时候,如果后边的命令依赖前边命令的执行结果,管道无法实现,脚本可以。
过期时间
expire key 50 50秒后key会自动删除
ttl key 查看key还有多少秒会被删除
persist key 取消对key过期时间的设置
当对key执行set命令的时候,也会取消对key过期时间的设置
pexpire与expire区别就是他的时间单位为毫秒
pttl查看剩余过期时间毫秒
排序
sort命令。
可以对列表、集合、有序集合进行排序,支持分页、倒叙。
在对有序集合排序时忽略分数。
sort key desc limit 1,2
加上参数alpha,可以对非数据类型进行字典排序
sort key alpha
sort key by xx->time desc
这是个散列表的查询,首先按照key模糊查询xx, 然后把结果按照time字段进行倒叙
消息通知
Redis的消息通知可以使用列表类型的LPUSH和RPOP(左进右出),
当然更方便的是直接使用Redis的Pub/Sub(发布/订阅)模式。
列表队列
redis通过列表实现这个功能,生产者从列表左边往队列push数据,消费者从队列右边pop数据。
实现任务队列,只需让生产者将任务使用LPUSH加入到某个键中,然后另一个消费者不断地使用RPOP命令从该键中取出任务即可
//生产者只需将task LPUSH到队列中
127.0.0.1:6379> LPUSH queue task
(integer) 1
//消费者只需从队列中LPOP任务,如果为空则轮询。
127.0.0.1:6379> LPOP queue
这种队列中没有数据的时候消费者直接返回,可以使用BLPOP使消费者阻塞
127.0.0.1:6379> BLPOP queue 0 //0表示无限制等待 , 当队列为空则处于阻塞状态
//生产者将task LPUSH到队列中,处于阻塞状态的消费者离开返回
127.0.0.1:6379> LPUSH queue task
(integer) 1
//消费者立刻“消费”,取出task。
127.0.0.1:6379> BLPOP queue 0
发布订阅
Redis支持发布/订阅的模式,"发布/订阅"模式中包含两种角色,分别是发布者和订阅者。
订阅者可以订阅一个或若干个频道(channel),发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
但是需要注意一下,使用发布订阅模式实现的消息队列,当有客户端订阅channel后只能收到后续发布到该频道的消息,之前发送的不会缓存,必须Provider和Consumer同时在线。
- PUBLISH:将信息 message 发送到指定的频道 channel。返回收到消息的客户端数量。
- SUBSCRIBE:订阅指定频道的信息。
一旦客户端进入订阅状态,客户端就只可接受订阅相关的命令SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE和PUNSUBSCRIBE除了这些命令,其他命令一律失效。 - UNSUBSCRIBE :取消订阅指定的频道,如果不指定,则取消订阅所有的频道。
127.0.0.1:6379> PUBLISH channel1.1 test
(integer) 0 //有0个客户端收到消息
127.0.0.1:6379> SUBSCRIBE channel1.1
Reading messages... (press Ctrl-C to quit)
1) "subscribe" //"subscribe"表示订阅成功的信息
2) "channel1.1" //表示订阅成功的频道
3) (integer) 1 //表示当前订阅客户端的数量
//当发布者发布消息时,订阅者会收到如下消息
1) "message" //表示接收到消息
2) "channel1.1" //表示产生消息的频道
3) "test" //表示消息的内容
//当订阅者取消订阅时会显示如下:
127.0.0.1:6379> UNSUBSCRIBE channel1.1
1) "unsubscribe" //表示成功取消订阅
2) "channel1.1" //表示取消订阅的频道
3) (integer) 0 //表示当前订阅客户端的数量
127.0.0.1:6379> PUBLISH channel1.1 test
(integer) 0 //有0个客户端收到消息
127.0.0.1:6379> SUBSCRIBE channel1.1
Reading messages... (press Ctrl-C to quit)
1) "subscribe" //"subscribe"表示订阅成功的信息
2) "channel1.1" //表示订阅成功的频道
3) (integer) 1 //表示当前订阅客户端的数量
//当发布者发布消息时,订阅者会收到如下消息
1) "message" //表示接收到消息
2) "channel1.1" //表示产生消息的频道
3) "test" //表示消息的内容
//当订阅者取消订阅时会显示如下:
127.0.0.1:6379> UNSUBSCRIBE channel1.1
1) "unsubscribe" //表示成功取消订阅
2) "channel1.1" //表示取消订阅的频道
3) (integer) 0 //表示当前订阅客户端的数量
//注:在redis-cli中无法测试UNSUBSCRIBE命令
持久化
redis支持两种持久化方式:
rdb:持久化数据, 默认持久化方式
aof:持久化命令
有三种时机会触发rdb备份:
- save触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止
- bgsave触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求,具体过程如下:
- redis使用fork函数复制一份当前进程(父进程)的副本(子进程)。
- 父进程继续接受客户端命令,子进程开始将内存数据写入到磁盘的临时文件中。
- 当副本进程写完所有数据到临时文件后,把dump.rdb文件替换成临时文件。
- 注意:fork函数执行后,父子进程还是共用相同内存(共享内存),并不会复制内存,此时如果父进程接受了新的客户端命令,则把发生改变的内存部分copy一份出来,保证共享内存不变,这样能节省内存空间。
- 自动触发,具体触发规则是可配置的
- save:比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
- stop-writes-on-bgsave-error:默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了
- rdbcompression ;默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储
rdb优点:
1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
缺点:RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据
接下来看aof
aof有三种触发机制:
(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)不同no:从不同步
aof优点:数据安全性更高
缺点:备份文件越来越大
两者可以同时开启
淘汰机制
可以设置redis最大可使用内存,当redis内存使用达到了这个内存后,就会按照配置策略执行。
redis有6种淘汰策略:
1、只淘汰设置了有效期的key,淘汰上次使用最早的,使用次数最少的key(基于lru算法)
2、只淘汰设置了有效期的key,随机淘汰key
3、只淘汰设置了有效期的key,淘汰剩余有效期最短的key
4、所有key都淘汰,淘汰上次使用最早的,使用次数最少的key(基于lru算法)
5、所有key都淘汰,随机淘汰key
6、不淘汰,直接拒绝些请求(默认)
线程模型
redis服务端有一个io多路复用器,该io多路复用器会监听多个套接字。
当客户端发起请求,服务端根据请求的类型生成相应事件对象,同时和相应的事件处理器关联。然后把事件对象放入到一个同步的队列中,io多路复用器以单线程方式从队列中拿事件对象交给事件分派器,事件分派器根据事件对象的类型将事件交给相应的处理器进行处理。当一个事件处理完毕后,io多路复用器继续从队列中拿下一个交给事件分派器。
图中事件分派器前边所有部分都是io多路复用器来实现的,底层是基于select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。
数据结构
redis是c语言开发的,不像java各种集合工具可以直接使用,所以redis自己实现了一系列数据结构,用来支持5大数据类型
简单动态字符串
在redis中,其自己定义了一种字符串格式,叫做SDS(Simple Dynamic String),即简单动态字符串。
sds有以下几个核心属性:
len:已使用的长度,即字符串的真实长度
alloc:除去标头和终止符(’\0’)后的长度
flags:低3位表示字符串类型,其余5位未使用
buf[]:存储字符数据
通过这几个属性可以发现,redis获取字符串长度为复杂度为o1,增加缓冲区减少更新内存次数。
双端链表
redis自己实现了一个双向链表
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;
字典
类比java的hashmap来介绍更加直观。
//HashMap
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
//Node
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
上面两个和java的HashMap是可以对应上的,接下来是redis特性的类:
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1
int rehashidx;
}dict;
- 我们关注dictht ht[2]; redis字典类型内部包含两个HashMap,第一个是真正使用的,第2个是用来扩容的。
扩容和hashMap有点区别,redis中是通过元素数量/容量计算出负载因子,然后结合当前redis是否进行持久化+负载因子来判断是否需要扩容。
渐进式rehash:
1)为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2)在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
3)在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
4)随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
跳表
https://blog.csdn.net/wei_gg/article/details/92407489
整数集合
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项
压缩列表
可以把他理解成java中的数组。
压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存
5大数据类型
Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redisObject 结构来表示:
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
//引用计数
int refcount;
//记录最后一次被程序访问的时间
unsigned lru:22;
}robj
对象的type属性记录了对象的类型,这个类型就是前面讲的五大数据类型:
对象的 prt 指针指向对象底层的数据结构,而数据结构由 encoding 属性来决定.
而每种类型的对象都至少使用了两种不同的编码:
可以通过如下命令查看值对象的编码:
OBJECT ENCODING key
比如 string 类型:(可以是 embstr编码的简单字符串或者是 int 整数值实现)
字符串
- 字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。
- 字符串支持三种编码
- int 编码,用来保存整数值,创建整数类型默认编码
- embstr 编码,用来保存短字符串,当创建字符串长度低于45时采用的编码,该编码是只读,当对字符串修改后,编码会升级到raw。embstr分配的redisObject和sds是一次分配的,他俩是连续的,而raw是分两次分配的。
- raw 编码,用来保存长字符串,当创建字符串长度达到45时采用的编码
散列类型
- 两种编码 ziplist 或者 hashtable
- 执行下边命令时
hset profile name "Tom"
hset profile age 25
hset profile career "Programmer"
3. 当列表保存元素个数小于512个,并且每个元素小于64字节时,采用压缩列表,否则就是hastable。这两个阀值是可以配置的
列表类型
- 列表类型可以想象成一个链表实现的有序队列,那么他的特征就是索引的时候会很慢,但是当从头或尾获取的时候很快。适合用来存储大数据量,但是操作只有从头和尾巴获取数据的类型,比如优惠券发券、抢红包。
- 列表对象的编码可以是 ziplist(压缩列表) 和 linkedlist(双端链表)
- 比如我们执行以下命令,创建一个 key = ‘numbers’,value = ‘1 three 5’ 的三个值的列表。
rpush numbers 1 "three" 5
4. 当列表保存元素个数小于512个,并且每个元素小于64字节时,采用压缩列表,否则就是双端链表。这两个阀值是可以配置的
集合类型
- 集合中元素无序、不可重复。
- 集合对象的编码可以是 intset 或者 hashtable
- intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。
- hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。
SADD numbers 1 3 5
SADD Dfruits "apple" "banana" "cherry"
5. 当集合对象中所有元素都是整数,且所有元素数量不超过512时采用intset
有序集合
- 在集合的基础上为集合中的每个元素关联了一个分数的属性,通过这个分数控制排序。
- 有序集合的编码可以是 ziplist 或者 skiplist。
- skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表,字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。
这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。 - 说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。
bitmap
适合场景:按天统计用户对某个网站的访问量,如果用户量级特别大,记录这些数据会占用大量内存。 bitmap适合此场景,节省内存,查询速度快。
比如网站每天有1亿用户访问量,用户id数据库自增。
当用户访问的时候执行Redis命令:setBit(日期,用户id,1),redis中存储的数据结构如下:
000010101010101
0代表未访问,1代表访问,索引代表用户id。
那么一个用户就每天占内存1bit,估算 1亿用户每天占内存:
(1亿 * 1bit ) / (8 * 1024 * 1024) M = 12m
非常节约内存。
还支持各种交集并集统计命令。
缺点:如果用户id不连贯,比如用户id100之后一下到5000万了,但是中间断档的用户也需要存储,这可能占用空间比其他数据结构还多。
集群
redis有三种高可用模式:复制(主从)、哨兵、集群。
主从
可以一主多从,主负责读写,从只负责读和同步主服务器数据。
适合读多写少的场景,主库单台作为写服务器,从可以无限扩充作为读服务器
原理:
无论redis采用什么方式的持久化,主从同步数据都是采用rdb的方式。
1、当从库启动后,会向主库发起sync命令
2、主库收到该命令后,开始生成快照,同时缓存客户端发过来的命令
3、然后主库将快照和缓存的命令发送给从库
4、从库收到后开始执行恢复快照操作,然后执行缓存的命令
上边是从库初次启动时执行的(叫做复制初始化),后续每当主库收到命令都会同步到从库(复制同步)。
当从与主断开重连之后redis2.6还是会执行一遍复制初始化,效率极其低下,redis2.6做了优化,断开重连后采用增量方式同步。
增量方式如何实现:复制同步期间,主库为每条命令都生产一个id,然后把所有命令放入一个队列中。从库拿到命令时也有会拿到这个id并且存起来,当断开连接重新请求的时候查出最后一条命令的id,发给主库,主库从队列中找出这个id后边的所有命令发给从库。
乐观复制:在复制同步阶段,主收到命令后是异步发给从库的,这样保证了从库不会影响主库性能,
但是:
1、主从数据就不是严格的一致了,但是会最终一致。
2、如果如果异步同步失败了,那么就会出现数据不一致了。
redis可配置当从库多少秒不与主库联系就代表从库断开连接。还可以配置当小于几个从库还是连接状态时,主库拒绝客户端请求。
哨兵
主从模式当主挂了之后,可以把一台从升级到主,但是这个升级需要人工来做。
哨兵模式提供了自动化的系统监控及故障恢复。
可以把哨兵看成一个单独部署的监控及故障恢复服务器,哨兵服务器也可以部署多台,他们之间也可以互相监控。最好为每台服务器部署一个哨兵节点。
原理
1、哨兵启动后通过配置文件查出主库信息
2、然后建立一个连接订阅主服务的哨兵频道,来获取当前有哪些哨兵在监控主服务器。
因为redis规定客户端一旦订阅了服务器,则该连接就不能执行其他命令,所以哨兵服务器会另外建立一个连接来执行其他命令
3、每隔10秒通过info命令来向主服务器获取信息。
该信息包括有哪些从服务器。
4、然后向所有主从建立两个连接,如上。
4、每2秒哨兵向主服务器的哨兵频道发送自己的信息。
到这一步所有的哨兵节点通过订阅所有主从服务器的哨兵频道实现了他们之间的信息共享。
5、每1秒向主从服务器发送ping命令监控服务器存活信息
6、当超过了指定时间ping命令仍然没有返回,如果节点为主服务器,则哨兵会认为这台服务器主观下线了
7、然后哨兵会询问其他哨兵节点是否也知道该节点主观下线了,如果达到指定数量,则哨兵认为该节点客观下线了。
哨兵发现节点客观下线后会发起故障修复。故障修复首先要通过raft算法选举一个领头哨兵,
由领头哨兵选取一个从库升级为主库,选区规则如下:
10、所有从库中选区一个优先级最高的(优先级是可配置的),如果有多个优先级最高的,则选择一个复制偏移量最大的(也就是从主库复制最多数据的)
集群
即使哨兵模式也是把所有数据存在单台服务器上,数据量过大,服务器内存压力依然存在。这时候就要使用集群模式了。可以理解成按照键的一定规则把数据分片,固定规则的数据固定落在一台服务器上。
注意如果部署了集群,则redis数据库只能使用0数据库。
配置
1、为每台服务器开启cluster-enabled。
接下来步骤都是通过redis辅助工具redis-trib.rb执行
2、将这些服务器加入到一个集群中。
3、分配主从关系
4、为每个主服务器分配插槽(redis会把所有数据按照key哈希分片到插槽中,然后在把插槽分配给集群的节点)
新节点增加
比如要把A节点加入到集群中。首先向A发送命令
ip端口为集群中任意一个节点(B),A收到命令后与B握手,B通知集群的其他节点,A加入到集群。
插槽在分配
A节点加进来可以选择以从库的身份运行在集群中,如果要以主库的身份运行还需要给他分配一个插槽。
redis一个集群总共有16384个插槽,每个主库处理部分插槽。
如果要给A分配的插槽是空闲的,之前没分配给其他服务器过,则直接分配即可。
如果插槽之前分配给其他服务器过,则就要做插槽迁移。
插槽迁移的时候数据通过命令迁移。
节点下线:
类似哨兵的下线过程。
集群中所有节点每隔1秒随机选择5个节点,选择其中一个响应时间最长的节点,发送ping命令。
分布式锁
redis命令实现锁的三种方式
incr:key的默认值未0,该命令如果返回1证明获取锁成功,返回2获取失败。 然后expire命令释放锁
setnx:用expire命令释放锁
set:setnx和expire不是原子操作, set可以直接指定过期时间(原子操作)
set在使用中可能出现的一些问题。
setnx包含两个操作,1、判断是否存在,2、设置值。这两个操作不是原子操作,会出现问题。
set(key,value,过期时间)该命令是用来解决该问题的。
有几个问题需要注意:
线程获取锁的时候失败了,怎么办,循环请求还是中断?
如果循环请求的话,多个线程容易发生抢锁的情况,太影响性能,怎么办?
如果线程获A取到锁,过期时间设置未30秒,但是线程执行时间为50秒,到30秒的时候A释放锁,线程B获取到锁,到50秒的时候A把锁删除,那么B也没有锁了,怎么办?
释放锁的时候:A线程检查锁是否为自己持有,假设正好在30秒的时候开始检查,是自己持有,31秒的时候锁被线程B获取,然后线程A释放锁就把B的锁释放了
1、循环请求
2、每次循环后sleep几毫秒
3、set锁的时候,不只要指定key,也要指定value,value代表哪个线程,释放锁的时候判断一下value是否为当前线程。
4、通过lua脚本把检查锁是否为自己所有和释放锁变成原子操作(eval函数可以把lua脚本直接交给redis服务器执行)