Redis总结
Redis是一个开源的key-value存储系统,和Memcached类似,Redis支持存储的value类型相对更多,包括string、list、set、zset和hash,这些数据类型都支持push/pop、add/remove及取交并差集及更丰富的操作,而且这些操作都是原子的
在此基础上,Redis支持各种不同方式的排序,与memcached一样,为了保证效率,数据都是混存在内存中,区别的是,Redis会周期性吧更新的数据写入磁盘或者修改操作写入追加的记录文件
安装配置
官网下载安装包,采用6.2.1版本
在解压安装前,由于redis依赖C语言开发环境,所以先安装相关依赖
yum -y install centos-release-scl scl-utils-build
yum -y install devtoolset-8-toolchain
scl enable devtoolset-8 bash
gcc安装成功
gcc安装完成后就可以解压安装包了
解压完成后,进入安装目录,执行make
命令进行编译
接下来安装软件,(在安装目录下)执行命令make install
(可以执行make install PREFIX=<path>
指定路径)
到这里Redis就安装成功了,会默认安装在/usr/local/bin
目录下
进入该目录下,可以看到有如下文件:
软件 | 作用 |
---|---|
redis-benchmark | 性能测试工具,可以在自己本子运行,看看子集本子性能如何 |
redis-check-aof | 修复有问题的AOF文件 |
redis-check-rdb | 修复有问题的RDB文件 |
redis-cli | 客户端操作入口 |
redis-sentinel | Redis集群使用 |
redis-server | Redis服务器启动命令 |
启动方式
- 前台启动(不推荐,命令行关闭后服务关闭):
redis-server
- 后台启动:
- 将Redis家目录下的redis.conf配置文件复制一份到etc目录下(也可不复制):cp redis.conf /etc/redis.con
- 修改etc目录下的redis.conf配置文件,将
daemonize no
改为daemonize yes
- 后台启动:
/usr/local/bin/redis-server /etc/redis.conf
- 启动完成后可以执行
ps -ef | grep redis
命令查看redis进程号,由于基于C语言环境,Redis进程无法使用JPS
命令查看到
使用redis-cli
命令可以连接Redis服务
基本配置项说明
# 单位设置方式,不区分大小写,并非注释项,提示如何书写注释
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
# 绑定连接IP,有这一行表示只允许本机连接,若要允许远程连接,需要注释这一行
bind 127.0.0.1 -::1
# 表示开启保护模式,不支持远程访问,若要允许远程连接,需要改为no
protected-mode yes
# 端口号 默认5379
port 6379
# 511表示正在进行三次握手建立TCP连接和已经建立TCP连接的队列总和
tcp-backlog 511
# 超时时间,如果有一段时间不操作,就会与redis断开连接,设为0表示不超时
timeout 0
# 心跳检测周期
tcp-keepalive 300
# Redis启动方式,是否为后台启动
daemonize yes
# 保存Redis进程号的文件,有四个级别:debug、verbose、notice、warning
pidfile /var/run/redis_6379.pid
# redis中日志级别
loglevel notice
# 设置日志的输出路径,默认为空,此时日志将输出到/dev/null
logfile ""
# 默认数据库数量
databases 16
# 设置密码,默认是注释掉的,需要设置密码的话取消注释
# requirepass foobared
# Redis同时最多可以和多少客户端连接,默认无设置
# maxclients 10000
# 最大样本数量
# maxmemory-samples 5
基本信息及操作指令
基本信息
- Redis默认端口号是6379(可以在配置文件中修改)
- 默认16个数据库,类似数组下标从0开始,初始默认使用0号库(可以在配置文件中修改)
- 统一密码管理,所有库同样密码
单线程多路复用
Redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一线程里执行,也可以启动线程执行(比如使用线程池)。
串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)
一个线程去执行真正的操作(数据库相关),其余线程只负责通知该线程执行的任务,其余线程通知任务是异步操作,在工作线程获得结果后,会去通知发布任务的线程
Key值操作
指令 | 含义 |
---|---|
keys <pattern> | 查看所有符合传入正则表达式规则的Key,常用keys * 查看所有Key |
exists <key> [key ...] | 判断某个key是否存在,可传入多个,有一个存在返回1,都不存在返回0 |
type <key> | 查看指定key是什么数据类型 |
del <key> [key ...] | 删除指定key数据,可以同时删除多个 |
unlink <key> [key ...] | 根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作 |
expire <key> <seconds> | 为给定的key设置过期时间,时间单位为秒 |
ttl <key> | 查看还有多少秒过期,-1表示永不过期,-2表示已过期 |
select <index> | 切换到指定索引号的数据库 |
dbsize | 查看当前数据库的key的数量 |
flushdb [ASYNC|SYNC] | 清空当前库,可指定同步异步操作 |
flushall [ASYNC|SYNC] | 清空全部库,可指定同步异步操作 |
常用五大数据类型
String
简介
String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个Key对应一个Value。
String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
常用命令
命令 | 含义 |
---|---|
set <key> <value> | 添加键值对,类似于map键值对操作,新增的键值对如果键已存在,会变为覆盖操作 |
get <key> | 获取指定键对应的值,没有键会返回空(nil) |
append <kye> <value> | 将给定的<value> 追加到原值的末尾 |
strlen <key> | 获取值的长度 |
setnx <key> <value> | 只有在key不存在时,设置key的值 |
incr <key> | 将Key中存储的数字值增加1,只能对数字值操作,如果为空,新增值为1 |
decr <key> | 将key中存储的数字值减一,只能对数值操作,如果为空,新增值为-1 |
incrby / decrby <key> <step> | 将key中储存的数字值增减,自定义的步长。注意:incr 和decr 是原子操作 |
mset <key1> <value1> <key2> <value2>...... | 同时设置一个或者多个键值对 |
mget <key1> <key2> <key3>...... | 同时获取一个或者多个value |
msetnx <key1> <value1> <key2> <value2> ...... | 同时设置一个或者多个键值对,当前进党所有给定的key都不存在时操作成功 |
getrange <key> <start> <end> | 获取值的方位,类似与java中的substring,左闭右闭 |
setrange <key> <start> <value> | 用<value> 覆写<key> 所存储的字符串值,从<start> 开始(索引从0开始) |
setex <key> <过期时间> <value> | 设置键值的同时,设置过期时间,单位秒 |
getset <key> <value> | 以新换旧,设置了新值同时获得旧值 |
数据结构
String的数据结构是简单的动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时只会多扩容1M,需要注意的是字符串最大长度是512M。
List(列表)
简介
特点:单键多值
Redis列表时简单的字符串列表,按照插入顺序排序(有序的),你可以添加一个元素到列表的头部或者尾部
它的底层实际是一个双向链表,对两端的操作性能都很高,通过索引下标的操作中间的节点性能会较差
常用命令
命令 | 含义 |
---|---|
lpush/rpush <key> <value1> <value2> ... | 从左边/右边插入一个或多个值,lpush每次都将value放在列表头,rpush每次都将value放在列表尾 |
lpop/rpop <key> | 从左边/右边吐出一个值。值在键在,值光键亡 |
rpoplpush <key1> <key2> | 从key1列表右边吐出一个值,插入key2列表的左边 |
lrange <key> <start> <pop> | 按照索引下标获得元素(从左向右),索引可以是负值,例如-1表示从右向左第一个,-2表示从右向左第二个 |
lindex <key> <index> | 按照索引下标获得元素(从左向右) |
llen <key> | 获得列表长度 |
linesert <key> before|after <value> <newvalue> | 在value的前/后面插入newvalue插入值 |
lrem <key> <n> <value> | 从左边删除n个value(只删除指定的值),n可以取0(不限制个数,全部删除)或者负数(代表从右边开始删除) |
lset <key> <index> <value> | 将列表key下表为index的值替换成value |
数据结构
List的数据结构为快速链表quickList
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构式ziplist,也即是压缩列表
它将所有的元素紧挨着一起存储,分配的是一块连续的内存
当数据量比较多的时候才会改成quicklist。
因为普通链表的附加指针空间太大,会比较浪费空间。比如这个列表中存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余
Set(集合)
简介
set对外提供的功能和list很相似,特殊之处在于set是自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个程序是否在一个set集合内的重要接口,这个也是list所不能提供的。
set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
一个算法,随着数据的增加,执行的时间长短,如果是O(1),数据增加,查找数据的时间不变
常用命令
命令 | 含义 |
---|---|
sadd <key> <value1> <value2>...... | 将一个或多个member加入到集合key中,已存在的member将被忽略 |
smembers <key> | 查看该集合中的所有值 |
sismember <key> <value> | 判断集合key是否含有value,有1,没有0 |
scard <key> | 返回该集合的元素个数 |
srem <key> <value1> <value2> ... | 删除集合中的某个元素 |
spop <key> | 随机从该集合中吐出一个值 |
srandmember <key> <n> | 随机从该集合中查看n个值。不会从集合中删除 |
smove <source> <destination> value | 把集合中的一个值value从source中移动到destination中 |
sinter <key1> <key2> | 返回两个集合的交集元素 |
sunion <key1> <key2> | 返回两个集合的并集元素 |
sdiff <key1> <key2> | 返回两个集合的差集元素(key1中的,不包含key2) |
数据结构
set数据结构是dict字典,字典是用哈希表实现的。
Java中的HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象,Redis的set结构也是一样的,它的内部也适用hash结构,所有的value都指向同一个内部值
Hash(哈希)
简介
hash是一个键值对集合
hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似于Java里面的Map<String,Object>
常用命令
命令 | 含义 |
---|---|
hset <key> <field> <value> | 给key集合中的field赋值value |
hget <key> <field> | 从key集合field中取出value |
hmset <key> <field1> <value1> <field2> <value2> | 批量设置hash的值 |
hexists <key> <field> | 查看哈希表key中是否包含指定field |
hkeys <key> | 列出该hash集合中所有的field |
hvals <key> | 列出该hash集合中所有的value |
hincrby <key> <field> <increment> | 为hash的key中的field的值增加increment |
hsetnx <key> <field> <value> | 当key不存在时,才进行field-value的赋值操作 |
数据结构
hash类型对应的数据结构是两种:zip(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
Zset
简介
有序集合zset和普通集合set非常相似,是一个没有重复元素的字符串集合
不同之处是有序集合的每个成员都关联了一个评分,这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。
因为元素是有序的,所以可以很快的根据评分或者次序来获取一个范围的元素
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的只能列表
常用命令
命令 | 含义 |
---|---|
zadd <key> <score1> <value1> <score2> <value2>... | 将一个或多个member元素及其score值加入到有序集中 |
zrange <key> <start> <stop> [WITHSCORES] | 返回有序集中,下标在start和stop之间的元素;当带有WITHSCORES,可以让分数一起和值返回到结果中 |
zrangebyscore key min max [withscores] [limit offset count] | 返回有序集中,所有score值介于min和max之间(左右均包括)的member。有序集成员按score值递增次序排序 |
zrevrangebyscore key max min [withscores] [limit offset count] | 同上,改为从大到小排序 |
zincrby <key> <increment> <value> | 为元素的score加上增量 |
zrem <key> <value> | 删除指定值的元素 |
zcount <key> <min> <max> | 统计集合中,指定区间内元素个数 |
zrank <key> <value> | 返回该值在集合中的排名,从0开始 |
数据结构
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String,Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名词,还可以通过score的范围来获取元素的列表
zset底层使用了两个数据结构
- hash,hash的作用就是关联value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score
- 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表,跳跃表从链表中提取了一些索引,并在索引的基础上再次提取了索引,可以快速定位元素
Jedis操作
导入maven依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
测试连接
// 创建Jedis对象
Jedis jedis = new Jedis("num04",6379);
// 测试连接
String value = jedis.ping();
System.out.println(value);
// 关闭连接
jedis.close();
能输出“PONG”代表成功连接到了Redis服务
如果连接失败,查看Redis启动配置文件中下面两项配置是否正确
- bind 127.0.0.1 -::1 ==>该项需要被注释掉
- protected-mode no ==>该项需要是no
基本方法演示
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.Set;
public class BasicDateType {
// 查看全部已有key
@Test
public void listKeys() {
Jedis jedis = new Jedis("num04", 6379);
Set<String> keys = jedis.keys("*");
for (String key : keys) {
System.out.println(key);
}
}
// 添加String类型的KV
@Test
public void addStringKey() {
Jedis jedis = new Jedis("num04", 6379);
jedis.set("name", "lucy");
}
// 获取String类型的Value
@Test
public void getStringKey() {
Jedis jedis = new Jedis("num04", 6379);
String nameValue = jedis.get("name");
System.out.println(nameValue);
}
// String类型mset同时设置多个KV
@Test
public void msetString() {
Jedis jedis = new Jedis("num04", 6379);
jedis.mset("k1", "v1", "k2", "v2");
}
// String类型mget同时获取多个V
@Test
public void mgetString() {
Jedis jedis = new Jedis("num04", 6379);
List<String> values = jedis.mget("k1", "k2");
for (String value : values) {
System.out.println(value);
}
}
// 操作List集合
@Test
public void testList() {
Jedis jedis = new Jedis("num04", 6379);
jedis.lpush("key1","value1","value2","value3");
List<String> values = jedis.lrange("key1", 0, -1);
for (String value : values) {
System.out.println(value);
}
}
// 操作Set集合
@Test
public void testSet(){
Jedis jedis = new Jedis("num04", 6379);
jedis.sadd("skey1","lucy","jack","jack");
Set<String> values = jedis.smembers("skey1");
for (String value : values) {
System.out.println(value);
}
}
// 操作hash
@Test
public void testHash(){
Jedis jedis = new Jedis("num04", 6379);
jedis.hset("users","age","20");
String age = jedis.hget("users", "age");
System.out.println(age);
}
// 操作ZSet
@Test
public void testZSet(){
Jedis jedis = new Jedis("num04", 6379);
jedis.zadd("china",100d,"shanghai");
Set<String> res = jedis.zrange("china", 0, -1);
System.out.println(res);
}
// 清空DB
@Test
public void flush(){
Jedis jedis = new Jedis("num04", 6379);
String res = jedis.flushDB();
System.out.println(res);
}
}
可以看到,Jedis对象的方法中基本包含了命令行中全部的可用命令,所有命令行的操作都可以简单便捷的转换为Jedis操作,其余指令就不再一一列举了
持久化操作
RDB
基本定义
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot块中,恢复时是将快照文件直接读到内存里
基本配置
生成的文件dump.rdb
默认在启动redis服务时所在的目录。
该项在配置文件SNAPSHOT下有配置:dir ./
同时有快照生成时间间隔:
Unless specified otherwise, by default Redis will save the DB:
* After 3600 seconds (an hour) if at least 1 key changed
* After 300 seconds (5 minutes) if at least 100 keys changed
* After 60 seconds if at least 10000 keys changed
You can set these explicitly by uncommenting the three following lines.
save 3600 1
save 300 100
save 60 10000
上面的配置默认是注释掉的,文档说明了快照生成的规则,key修改的越多,快照生成越快
以save 3600 1
为例,再3600秒内,如果有1个key修改了,就保存快照
如果300秒内修改了102个key,那么只会持久化100个,后面2个算在后面的持久化周期内
savemode
save:save时只管保存,其他不管,全部阻塞,手动保存。不建议使用
bgsave:Redis会在后台异步进行快照操作,快照同时还可以相应客户端请求。
可以通过lastsave
命令获取最后一次成功执行快照的时间
flushall
执行flushall命令也会生成快照文件
stop-writes-on-bgsave-error
默认yes,当Redis无法写入磁盘时,直接关闭Redis写操作,推荐yes
rdbcompression
对于存储到磁盘中的快照,可以设置是否进行压缩存储,如果是的话,redis会采用LZF算法进行压缩
默认为yes
rdbchecksum检查完整性
默认yes,推荐yes
在存储快照后,使用CRC64算法进行数据校验
但是这样会增减大约10%的性能消耗,如果希望获取最大的性能提升,可以关闭此功能
备份是如何执行的
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据回复的完整性不是很敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失
Fork
- Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数等)数值都和原进程一直。但是是一个全新的进程,并作为原进程的子进程
- 在Linux程序中,fork会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux引入了“写时复制技术”
RDB的恢复操作
关闭Redis
先把备份的文件拷贝到工作目录下
启动Redis,备份数据会直接被加载
RDB的优缺点
优点
- 适合大规模的数据恢复
- 对数据的完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
缺点
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down的话,就会丢失最后一次快照后的所有修改
停止RDB:redis-cli config set save""#save
后给空值,表示禁用保存策略
AOF持久化策略
Append Only File
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区内;
- AOF缓冲区根据AOF持久化策略(always、everysec、no)将操作sync同步到磁盘的AOF文件中;
- always:始终同步,每次Redis的写入都会立刻记录日志;性能较差但数据完整性比较好
- everysec:每秒同步,每秒记入日志依次,如果宕机,本秒的数据可能会丢失
- no:不主动进行同步,把同步的时机交给操作系统
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
- Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的
AOF默认是不开启的,需要修改配置文件修改为开启appendonly yes
和
AOF文件目录和RDB文件生成目录相同,可以修改生成文件的名称:appendfilename xxx.aof
AOF和RDB同时存在时,系统会使用AOF备份数据
AOF文件的备份恢复和RDB相同,只需要复制AOF文件,在恢复时将备份的文件放回到数据目录下就可以了
异常恢复
- 修改默认的appendonly no,改为yes
- 如遇到AOF文件损坏,通过
/usr/local/bin/redis-check-aof --fix appendonly.aof
进行恢复 - 备份被写坏的AOF文件
- 恢复:重启redis,然后重新加载
AOF优缺点
AOF优点
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文件,通过操作AOF文件,可以处理误操作
缺点
- 比起RDB占用更多的磁盘空间,
- 恢复备份速度更慢
- 每次读写都同步的话,有一定性能压力
- 存在个别BUG,造成恢复不能成功
选用策略
- 官方推荐两个都启用
- 如果对数据不敏感,单独选用RDB
- 不推荐单独启用AOF
多结点部署
主从模式
主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主
主从模式的作用
- 压力分摊
- 容灾机制
主从模式配置
- 复制一份配置文件到单独的目录下:
mkdir /myredis
,cp /etc/redis.conf /myredis/redis.conf
- 为每个结点配置个配置文件,按照配置项配置,计划使用6379、6380和6381三个端口,所以创建了三个配置文件:
redis6379.conf
、redis6380.conf
、redis6381.conf
include /myredis/redis.conf # 刚才复制的主配置文件
pidfile /var/run/redis6379.pid # 后缀和启动端口号一致即可
port 6379 # 启动端口
dbfilename dump6379.rdb # 持久化文件名称
- 配置完成后启动三个redis服务:
redis-server /myredis/redis6379.conf
逐个启动redis-server服务 - 启动后使用
ps -ef | grep redis
命令查看,有三个服务启动
- 三台结点登录redis客户端服务,执行
info replication
命令可以查看当前结点的集群情况,现在三台结点应该都是主节点,无从节点 - 在从节点上执行命令
slaveof <ip><port>
,指定主节点的IP和端口后,该节点将变为从节点。重新执行命令查看情况
8.此时可以做一个测试,在主节点上创建一个key,在其他从节点都可以查看,但从结点此时无法创建key了
主从模式原理
当从服务器宕机后,剩余服务器不会受影响,只是从节点减一,但是宕机的服务器重启后,它将不再是从服务器,而是主服务器,且不会保有当前机器宕机后主服务器写入的数据,只有当再次执行slaveof <ip> <port>
后才能重新称为从服务器,并同步主服务器数据
当主服务器宕机后,从服务器中不会产生新的主服务器,此时仍可执行查询操作。待主服务器重启后,它仍是集群的主服务器,且保有全部的从服务器
原理
- 当连接上主服务器之后,从服务器向主服务器发送数据同步请求;
- 主服务器接收到请求后,会将主服务器中的数据进行持久化,并发送持久化的RDB文件发送从服务器,从服务器拿到RDB进行读取
- 每次主服务器进行写操作后,会和从服务器进行数据同步
主从模式还有以下几种部署模式:
-
上一个Slave可以是下一个slave的Master(依旧使用
slaveof <ip> <port>
命令),Slave同样可以接收其他slaves的连接同步请求,那么该slave作为了链条中的一个mater,可以有效减轻master的同步压力,去中心化降低风险 -
即将从服务器转换为主服务器(
slaveof no one
指令),但实际这种指令和Hadoop
的高可用不一样,从服务器切换为主服务器后是一个单结点,并不会作为其他从服务器的主服务器存在 -
哨兵模式(sentinel)
第二种模式的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
配置步骤
- 调整为一主二仆模式。
- 在
/myredis
目录下创建sentinel.conf
文件,名字绝不能错 - 配置文件内容:
sentinel monitor mymaster 127.0.0.1 6379 1
- mymaster是为监控的服务器起的名字,1表示至少有多少个哨兵认为主服务器故障了,同一迁移
- 启动哨兵:
redis-sentinel /myredis/sentinel.conf
- 此时被监控的主机故障后,一段时间之后,将会从其从服务器中选择一个作为新的主机
- 原主机重启后会变为从机
复制延迟
由于所有的写操作都是先在Master上操作,然后同步到Slave上,所以从Master同步到Slave机器会有一定的延迟,当系统很繁忙的时候,延迟问题会更见严重,Slave机器数量的增加也会使这个问题更加严重
故障恢复过程
故障恢复时,选择作为新主服务器的从服务器有一个选择条件,依次是(优先按照前面的条件选择,当前面条件相同时,逐个向后比较):
- 选择优先级靠前的
- 选择偏移量最大的
- 选择runid最小的
优先级在redis.conf
中设置,配置项为:replica-priority 100
,即默认值为100,值越小优先级越高
偏移量是指获得原主机数据最全的
runid指每个redis实例启动后随机生成的进程ID
主备模式
Redis集群实现了对Redis的水平扩容,即启动N个结点,将整个数据库分布存储在这N个节点中,每个结点存储总数据的1/N。
Redis集群通过分区(partitions)来提供一定程度的可用性(availability):即使集群中有一部分结点失效或者无法进行通讯,集群也可以继续处理命令请求。
搭建
- 需要开启
daemonize yes
- 每个结点创建一个配置文件,配置内容
include /myredis/redis.conf
pidfile "/var/run/redis6379.pid"
port 6379
dbfilename "dump6379.rdb"
cluster-enabled yes # 表示打开集群模式
cluster-config-file nodes-6379.conf # 生成文件名
cluster-node-timeout 15000 # 连接超时时间
- 将修改好的配置文件拷贝多份,并修改对应内容
- 启动6个redis服务
5. 将6个结点合成一个集群
- 组合前,确保redis实例启动后,nodes-xxx。conf(配置的文件名)文件正常生成
- 进入redis最初解压安装路径下的src目录下:`cd /opt/redis/src`
- 执行合成命令(这里不要使用127.0.0.1):
./redis-cli --cluster create --cluster-replicas 1 192.168.226.40:6379 192.168.226.40:6380 192.168.226.40:6381 192.168.226.40:6389 192.168.226.40:6390 192.168.226.40:6391
// 上面的1表示用最简单的方式创建集群,一主一仆搭配
- 合成完成后可以看到如下界面:
- 此时要登陆需要使用集群登录方式:
redis-cli -c -p 6379
- 登录成功后使用
cluster nodes
命令查看集群状态信息
redis cluster结点分配
一个集群至少要有三个结点
选项-cluster-replicas 1
表示我们希望为集群中的每个主结点创建一个从结点
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP低智商
slots
上面cluster nodes
命令可以看到ALL 16384 slots covered
一个Redis集群包含16384个插槽,数据库中每个键都数据这些插槽中的一个
集群使用公式CRC16(KEY) % 16384
来计算key属于哪个插槽,其中CRC16(key)
语句用于计算key和CRC16校验和
集群中每个结点负责处理一部分插槽,举个例子,如果一个集群可以有主结点,其中
结点A负责处理0-5460号插槽;
结点B负责处理5461-10922号插槽;
结点C负责处理10923-16383号插槽。
故障恢复
当主节点下线,从节点将自动升为主节点,但是需要15秒超时时间过后
主节点恢复后,将做为从节点继续提供服务
如果某一段插槽的主从节点都宕掉,redis服务是否继续取决于cluster-require-full-coverage
如果值为yes,那么整个集群宕机,停止服务
如果值为no,那么对应插槽数据不能使用,其余位置可以正常使用
集群操作
集群中不能直接使用mset
,mget
等多键操作。
需要在每个键后面使用{}
,来定义组,集群键根据组算出分配的插槽位置,例如:mset name{user} lucy age{user} 20 address{user} china
查询某个键的插槽编号:cluster keyslot <key>
查询value数量:cluster countkeysinslot 12706
查询插槽中的键:CLUSTER GETKEYSINSLOT <slot> <count>
注意:只能看到当前所在服务器所属的插槽范围内的key内容
集群的Jedis操作
除了要获取专门的JedisCluster
对象外,其余操作一致
public static void main(String[] args) {
HostAndPort hostAndPort = new HostAndPort("num04", 6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
jedisCluster.set("k10", "v10");
String value = jedisCluster.get("k10");
System.out.println(value);
jedisCluster.close();
}
主备模式优缺点
优点
- 水平扩容
- 分摊压力
- 无中心配置相对简单
缺点
- 多键操作不支持
- 多键的Redis事务是不被支持的。lua脚本不被支持
- 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大
常见应用问题
缓存穿透
问题描述
正常服务器接收到请求后,会先向Redis查询数据,如果Redis中不存在,会再向数据库中查询
当应用服务器压力增大,请求查询项Redis中查询数量降低(命中率降低),导致大量请求直接查询数据库,最终导致数据库瘫痪。
原因
- redis查询不到数据了
- 请求中包含了大量的非正常url(被黑客攻击,这种情况居多)
解决方案
- 对空值做缓存
如果一个查询的返回数据为空(不管数据是否不存在),我们扔把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。 - 设置可访问的白名单:
使用bitmaps类型定义一个可访问白名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问 - 采用布隆过滤器
(布隆过滤器(Bloom Filter))是1970年由布隆提出的,它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一半的算法,缺点是有一定的误识别率和删除困难
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力 - 进行实时监控
当发现Redis的命中率开始急速降低,需要排查访问对象和访问数据,和运维人员配合,可以设置黑名单限制服务
缓存击穿
问题描述
- 数据库访问压力瞬时增加
- redis里面没有出现大量的key过期
- redis正常运行
原因
redis某个key过期了,而碰巧该key被大量访问
解决方案
- 预先设置热门数据
在redis高峰访问之前,把一些热门数据提前存入到redis中,加大这些热门数据的时长 - 实时调整
现场监控哪些数据热门,实时调整key的过期时长 - 使用锁
就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
当操作返回成功时,在进行load db的操作,并回设缓存,最后删除mutex key;
当操作返回失败时,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法
缓存雪崩
问题描述
正常访问
缓存失效
- 数据库压力变大
- 服务器崩溃
原因
在极少时间段,查询大量key的集中过期情况
解决方案
- 构建多级缓存架构
nginx缓存+redis缓存+其他缓存(ehcache等) - 使用锁或队列
用加锁或者队列的方式来保证不会有大量线程对数据库一次性进行读写,从而避免失效时大量的并发请求落在底层存储系统上,不适用高并发情况 - 设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存 - 将缓存失效的时间分散开
可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的时间
分布式锁
问题描述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程,多进程并且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
目前分布式锁的主流实现方案:
- 基于数据库实现分布式锁
- 基于缓存(redis等)
- 基于zookeeper
其中基于redis的实现性能最好,基于zookeeper的安全性最高
基于redis的实现通过setnx
和设置过期时间实现