Redis

Redis

  • 摘要
  • 1.快速开始
  • 2.数据类型与结构
  • 3.发布订阅
  • 4.事务处理
  • 5.持久化
  • 6.主从同步
  • 7.集群
  • 8.应用问题与解决
  • 9.Java整合Redis

摘要

本篇博客对Redis基础进行总结,以便加深理解和记忆。

Redis中文学习网

1.快速开始

1.概述

  • 默认端口为6379
  • 默认有16个数据库,index从0开始,默认使用0数据库。各数据库使用统一的密码管理
# 库级基本操作
# 切换数据库
select <dbid>
# 查看当前数据库key的数量
dbsize
# 清空当前数据库
flushdb
# 清空所有数据库
flushall
  • 单线程 + 多路IO复用

多路复用是用一个线程来检测多个文件描述符的就绪状态,如调用select和poll函数。传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞超时。得到就绪状态后执行操作时可以在同一个线程里进行,也可以启动线程进行。

注意比较:串行、多线程+锁、单线程+多路复用

2.下载&安装

1)Windows 教程 github

2)Linux

①离线上传安装

# 下载地址:https://download.redis.io/releases/
# 上传到Linux系统的/opt/目录
# 解压
tar -zvxf redis-6.2.1.tar.gz
# 安装gcc
yum install gcc
# 进入下载目录
cd redis-{6.2.1}
# 编译
make
# 安装
make install
# 查看安装目录
cd /usr/local/bin/
ll/dir

目录介绍:

  • redis-benchmark:性能测试工具(可以测试自己的电脑/服务器性能如何)
  • redis-check-aof:用于修复有问题的AOF文件
  • redis-check-dump:用于修复有问题的dump.rdb文件
  • redis-sentinel:redis集群使用
  • redis-server:redis服务器启动命令
  • redis-clit:redis客户端

3.启动服务端

1)Windows

①前台启动:运行redis-server.exe

2)Linux

①前台启动(不推荐,断开连接,则redis会停止)

redis-server

②后台启动(推荐)

# 复制redis.conf文件到/etc
cp /opt/redis-6.2.1/redis.conf /etc
# 修改配置文件:将daemonize设为yes
vim /etc/redis.conf
# 启动redis
redis-server /etc/redis.conf
# 查看redis进程
ps -ef | grep redis

4.关闭redis

# 方式1
kill -9 pid
# 方式2
redis-cli shutdown

5.进入redis客户端

redis-cli

6.redis命令参考 Redis 命令参考 Redis命令中心

2.数据类型与结构

redis的存储方式是k-v对,k均为字符串类型,value有不同类型

1.key

# 查看当前库中所有的key
keys *
# 判断某个key是否存在
exists key
# 查看key是什么类型
type key
# 删除制定的key
del key
# 根据value非阻塞删除key,仅仅将key从keyspace中删除,真正的删除会在后续的异步中进行
unlink key
# 为指定的key设置有效期
expire key {time}
# 查看指定的key还有多久过期:-1永不过期;-2已经过期
ttl key

2.字符串String

1)数据结构

String是二进制安全的,即可以包含任何数据,如图片或序列化对象

String的数据结构为简单动态字符串(simple dynamic string,SDS)是可以修改的字符串,内部结构类似于Java的ArrayList,采用分配冗余空间的方式来减少内存的频繁分配

Redis_第1张图片

为字符串分配的空间要大于实际占用,当字符串的长度小于1MB时,每次扩充为翻倍扩充,如果超过1MB,则每次扩充1MB的空间。注意,String的最大长度不能超过512MB

2)常用命令

# 1.添加键值对
# 参数1:超时时间,分别为s、毫秒、时间戳、毫秒时间戳
# 参数2:NX:当key不存在时才添加,XX:当key存在时才可添加
set key value [EX|PX|EXAT|PXAT|KEEPTTL] [NX|XX] [GET]
# 2.取值
get key
# 3.追加值(追加至字符串尾)
append key value
# 4.获取值的长度
strlen key
# 5.在key不存在时,再设值
setnx key value
# 6.原子增1(只能对数值操作,key存在增1,不存在则创建值为1)
incr key
# 7.原子减1(只能对数值操作,key存在减1,不存在则创建值为-1)
decr key
# 8.递增或递减指定数字
incrby/decrby key step
# 9.同时设置多个k-v对
mset key1 value1 key2 value2 ...
# 10.取多个值
mget key1 key2 ...
# 11.当多个值都不存在时,设值
msetnx key1 value1 key2 value2 ...
# 12.获取一定范围的值(类似于java的substring)
getrange key start end
# 13.覆盖指定位置的值
setrange key start value
# 14.设置键值和过期时间(S)
setex key <过期时间> value
# 15.以新换旧,返回旧值
getset key value

3.列表List

1)数据结构

redis的列表是字符串列表,按照插入顺序排序,可以将一个元素插入到列表的头或尾。其底层是用双向链表实现的,对双端的操作性能很高,通过索引下标操作中间节点的性能较差

在列表元素较少时会使用一块连续的内存存储,即压缩链表ziplist,它将所有的元素连续存储,分配的是一块连续的区域。当列表元素较多时,会变成quicklist。这是因为普通的链表需要的附加指针空间太大,会比较浪费空间。redis将链表和ziplist结合起来组成quicklist,即将多个ziplist使用双向指针串起来,这样既能满足快速插入的性能,又不会出现太大的空间冗余。

2)常用命令

# 1.从左或从右插入一个或多个值
lpush/rpush key1 value1 key2 value2 ...
# 2.从列表左获取指定范围的值。可以使用负索引
lrange key start stop
# 3.从左、右弹出多个元素。值在键在,值光键亡
lpop/rpop key count
# 4.从一个列表右边弹出一个元素放到另一个列表中
rpoplpush source destination
# 5.根据索引取值
lindex key index
# 6.获取列表长度
llen key
# 7.插入值
linsert key before|after value newvalue
# 8.删除指定数量的某值元素,count>0从左向右,<0从右向左,删除与value值相等的count个元素
lrem key count value
# 9.替换指定位置的值
lset key index value

4.集合set

1)数据结构

redis的set是一个无序集合,其底层是一个value为null的hash表,增删查的复杂度都是O(1)。set的数据结构是字典,字典是用hash表实现的,所有的value指向同一个内部值

2)常用命令

# 1.添加元素
sadd key value1 value2 ...
# 2.取出所有元素
smembers key
# 3.判断集合中是否有某个值
sismember key value
# 4.返回集合中元素的个数
scard key
# 5.删除多个元素
srem key member1 member2 ...
# 6.随机弹出多个值
spop key count
# 7.随机获取多个值,值不会从集合中删除
srandmember key count
# 8.将某个值从一个集合移动到另一个集合,原子操作
smove source destination memeber
# 9.取多个集合的交集
sinter key1 key2 ...
# 10.将多个集合的交集放到一个新的集合中
sinterstore destination key1 key2 ...
# 11.取多个集合的并集,自动去重
sunion key1 key2 ...
# 12.将多个集合的并集放到一个新集合中
sunionstore destination key1 key2 ...
# 13.取多个集合的差集
sdiff key1 key2 ...
# 14.将多个集合的差集放到一个新的集合中
sdiffstore destination key1 key2 ...

5.哈希hash

1)数据结构

redis的hash是一个k-v对集合,k、v均为string类型,适合存储对象(类似于Java中的Map)其数据结构为ziplist和hashtable,在元素较少时使用压缩列表、否则使用hashtable

2)常用命令

# 1.设值
hset key field1 value1 field2 value2 ...
# 2.获取指定field的值
hget key field
# 3.获取所有键和值
hgetall key
# 4.判断给定的键是否存在
hexists key field
# 5.列出所有的键
hkeys key
# 6.列出所有value
hvals key
# 7.返回键的数量
hlen key
# 8.给指定的键增加指定的增量,增量可以为负数,若键不存在则新建
hincrby key field increment
# 9.当键不存在时,才创建设值
hsetnx key field value

6.有序集合zset(sorted set)

1)数据结构

redis的zset是一个没有重复元素的字符串的有序集合,其每个元素都关联了一个评分,该评分被用来升序排序集合中的元素。元素是唯一的,但评分是可重复的。访问zset的中间元素速度也是很快的,因为有序集合可以看作一个无重复的智能列表。

zset内部用到了两种数据结构:

①hash表,key为集合中的元素,value为score(类似于java中的Map),可以用来快速定位元素定义的score,时间复杂度为O(1)

②跳表skiplist(类似于java中的ConcurrentSkipListSet),插入、删除、查找的时间复杂度均为O(logN)

Redis_第2张图片

7.位操作字符串Bitmaps

1)数据结构

Bitmaps并不是一种特殊的数据类型,它实际上就是字符串string,但是它可以对字符串的位进行操作,提供了一套单独的命令。可以将Bitmaps看作一个以位为单位的数组,每个位只能存储0和1,数组的下标在Bitmaps中被称为偏移量

Bitmaps实际是赋予了语义限制的数据结构,在一些场景下具有优势,如统计活跃用户,它适合于数据非稀疏时的场景,在数据稀疏时节省存储空间的效果可能并不很好

2)常用命令

# 1.设置某个偏移量的值
setbit key offset value
# 2.获取某个偏移位置的值
getbit key offset
# 3.统计bit位全为1的数量
bitcount key start end
# 4.对一个或多个bitmaps执行位操作
bitop [and|or|not|xor] destkey key1 key2 ...

8.HyperLogLog

HyperLogLog也是对于特定场景下提出的数据类型,它用于独立统计(如网页的独立访客统计而非网页的总访问量),这种求集合中不重复元素个数的问题称为基数问题。解决基数问题可以记录总访问量然后进行去重,但是这样存储全访问量会导致占用空间越来愈大。HyperLogLog就是在这样的场景和问题下被提出的,它用于损失一定精度来平衡存储空间,在输入元素的数量非常大时,其计算基数所需的空间总是固定的,且很小的

# 1.添加多个元素
pfadd key value1 value2 ...
# 2.获取多个HLL合并后元素的个数(统计一个或者多个key去重后元素的数量)
pfcount key1 key2 ...
# 3.将多个HLL合并后的结果放入另一个HLL
pfmerge destkey sourcekey1 sroucekey2 ...

9.Geographic

redis 3.2中增加了对GEO类型的支持,即地理信息。该类型即二维坐标,对应地图上的经纬度。redis基于这种类型,提供了相应的地理场景下的API

# 1.添加多个位置的经纬度,(longitude latitude member:经度 纬度 名称)
geoadd key longitude1 latitude1 member1 longitude2 latitude2 member2 ...
# 2.获取多个位置的坐标
geopos key member1 member2 ...
# 3.获取两个位置的直线距离(单位:[m|km|ft|mi] -》[米|千米|英里|英尺],默认为米)
geodist key member1 member2 [m|km|ft|mi]
# 4.以给定的经纬度为中心,找出某一半径内的元素
georadius key longitude latitude radius [m|km|ft|mi]

3.发布订阅

客户端可以订阅任意个频道,客户端可以向某一频道发送消息,订阅该频道的客户端将会收到消息

# 1.订阅频道,返回值为接收到的信息(返回值类型、订阅频道的名称、当前已订阅的频道数量)
subscribe channel1 channel2 ...
# 2.发送消息,返回值为接收到该message的订阅者数量
publish channel message
# 3.订阅一个或多个符合给定模式的频道,如使用*的通配符 news.*,订阅以news.开头的所有频道
psubscribe pattern1  pattern2
Redis_第3张图片

如上例所示,在windows下前台运行redis服务端,开启两个redis客户端订阅channel,再开启一个redis客户端向channel发送消息

4.事务处理

1)介绍

redis对事务进行单独的隔离,事务中所有的命令都会被序列化、按顺序地执行,事务在执行过程中不会被其他客户端发送的命令所打断。redis事务的主要作用就是串联多个命令来防止别的命令插队。

redis事务分为两个阶段:

  • 组队阶段:只将所有命令加入到命令队列
  • 执行阶段:依次执行队列中的命令,执行过程中不会被其他客户端发送的命令插队或打断
Redis_第4张图片

2)特性

  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请 求所打断
  • 没有隔离级别的概念:队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执 行
  • 不能保证原子性:事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
    • 如果在组队阶段,有1个失败了,后面都不会成功
    • 如果在组队阶段成功了,在执行阶段有那个命令失败 就这条失败,其他的命令则正常执行,不保证都成功或都失败

3)使用方法

# 1.开启一个事务块
multi
# 定义事务中的操作...
# 2.停止事务
discard
# 3.结束事务中操作的定义,开始执行
exec

4)解决redis事务的非原子性

为了一定程度解决redis在事务执行中不回滚的问题,可以使用watch对key进行监控。这是一种类似于乐观锁的方式(即不先加锁,而在真正的执行前进行值检查)。这样在事务执行中如果遇到key修改问题,该事务的所有操作则会取消执行

# 1.监控
watch key1 key2 ...
# 2.取消监控:如果在exec或discard命令被执行了的话,watch则无需取消监控,事务成功或被取消时,watch自动会被释放
unwatch

5.持久化

1.RDB(Redis DataBase)

1)RDB是指在指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot快照),恢复时是将快照文件直接读到内存中

2)Redis会单独创建(Fork)一个子进程进行持久化会先将数据写入到一个临时文件中,待持久化过程都结束后,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这 就是确保了极高的性能,如果需要进行大规模的恢复,且对数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失

3)Fork

Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器 等)数值都和原进程一致,它是一个全新的进程,并作为原进程的子进程。

一般情况父进程和子进程会共用一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

4)RDB配置

vim redis.conf
# 1.指定备份文件的名称
dbfilename dump.rdb
# 2.指定备份文件存放的目录
dir ./	# ./表示redis-server命令启动时的所在目录
# 3.自动备份的规则,s 次。默认为1分钟内修改了1万次,或5分钟内修改了10次,或30分钟修改了1次就进行备份
save 60 10000
save 300 100
save 3600 1
# 4.当磁盘满时,是否关闭redis的写操作
stop-writes-on-bgsave-error yes
# 5.RDB备份是否开启压缩
rdbcompression yes
# 6.是否检查rdb备份文件的完整性
rdbchecksum yes

5)手动备份和恢复

############# 备份 #############
# 1.阻塞备份
save
# 2.异步非阻塞备份
bgsave
# 3.获取最后一次生成快照的时间
lastsave
# 4.产生dump.rdb,但内容为空无意义
# flushall
############# 恢复 #############
# 5.查询rdb文件的位置
config get dir
# 6.将rdb备份文件复制到别的地方
cp dump.rdb dump2.rdb
# 7.恢复
# 7.1 关闭redis
# 7.2 将备份的文件拷贝到工作目录
# 7.3 启动redis,备份数据直接被加载,数据被恢复
############# 停止 #############
# 8.停止客户端进行RDB
redis-cli config set save ""

6)分析

①优势

  • 适合大规模数据恢复
  • 对数据完整性和一致性要求不高时更适合使用
  • 节省磁盘空间
  • 恢复速度快

②劣势

  • Fork的时候,内存中的数据会被复制一份,导致2倍的膨胀

  • 虽然Redis在Fork的时候使用了写拷贝技术,但数据庞大时还是比较消耗性能

  • 最后一次快照的全部修改容易丢失

2.AOF(Append Only File)

1)AOF以日志的形式记录每个写操作(增量保存),只允许追加但不可改写文件。redis启动之初会读该文件根据写操作从头到尾执行一次数据的恢复工作

  • 客户端的写命令会被append到AOF缓冲区中
  • AOF缓冲区会根据持久化策略[always|everysec|no]将操作同步到磁盘的AOF文件中
  • AOF文件大小超过重写策略或手动重写时,会对AOF文件进行重写,压缩AOF文件容量
  • redis服务器重启时,会加载AOF文件中的写操作以达到数据恢复的目的

2)配置文件

# redis.conf
############# 启动 #############
# 1.开启AOF,默认为不开启no
appendonly yes
# 2.AOF文件名称
appendfilename "appendonly.aof"
# 3.aof文件所在目录
dir ./
############# 同步 #############
# 4.同步频率设置
# 4.1 每次写入立即同步
appendsync always
# 4.2 每秒同步
appendsync everysec
# 4.3 不主动同步,将同步交给操作系统
appendsync no
############# 重写压缩 #############
# 5.设置重写基准率,即当新缓存文件大小超过原缓存文件大小多少时进行压缩,默认为100%,即达到原文件的2倍
auto-aof-rewrite-percentage 100
# 6.设置重写起始值,默认为64MB
auto-aof-rewrite-min-size 64
# 7.行AOF重写时不会将AOF缓冲区中的数据同步到旧的AOF文件磁盘,也就是说在进行AOF重写的时候,如果此时有写操作进俩,此时写操作的命令会放在aof_buf缓存中(内存中),而不会将其追加到旧的AOF文件中,这么做是为了避免同时写旧的AOF文件和新的AOF文件对磁盘产生的压力。
no-appendfsync-on-rewrite on

3)常用命令

############# 恢复 #############
# 1.正常恢复:同RDB恢复
# 2.异常恢复(遇到AOF文件损坏的问题)
/usr/local/bin/redis-check-aof --fix appendonly.aof
############# 重写压缩 #############
bgrewriteaof

4)AOF和RDB同时开启

AOF和RDB同时开启,系统默认取AOF的数据

5)重写压缩流程

  • 手动执行 bgrewriteaof 命令触发重写,判断是否当前有bgfsave或bgrewriteaof在运行,如果 有,则等待该命令结束后再继续执行
  • 主进程fork出子进程执行重写操作,保证主进程不会阻塞
  • 子进程遍历redis内存中的数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和 aof_rewrite_buf重写缓冲区保证原AOF文件完整性以及新AOF文件生成期间的新的数据修改动作不 会丢失
  • 子进程写完新的AOF文件后,向主进程发送信号,父进程更新统计信息
  • 主进程把aof_rewrite_buf中的数据写入到新的AOF文件
  • 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写

6)分析

①优势

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,通过操作AOF文件,可以处理误操作

②劣势

  • 比RDB占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别bug,造成不能恢复

3.如何选择持久化策略

  • 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式
  • 同时开启两种持久化方式:在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF
    文件保存的数据集要比RDB文件保存的数据集要完整
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那如果只使用AOF呢?建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段
  • 性能建议
    • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save 900 1 这一
    • 如果使用AOF,好处是在最恶劣的情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了
    • AOF的代价,一是带来持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据(aof_rewrite_buf)写到文件造成的阻塞几乎是不可避免的
    • 只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基数大小默认值64M(auto-aof-rewrite-min-size)太小了,可以设置到5G以上
    • 默认超过原大小100%(auto-aof-rewrite-percentage)大小时重写可以改到适当的数值

6.主从同步

1)开启多个redis服务端,划分为主服务端Master和从服务端Slave。Master以写为主,Slave只读

作用

  • 实现读写分离,可以进行性能的扩展,降低服务器的压力
  • 容灾,快速恢复,主机挂掉后,从机变为主机

原理

  • slave启动成功连接到master后,给master发送sync数据同步消息
  • master接收到slave发送的数据同步消息后,将主服务器的数据持久化到rdb文件,同时收集从客户端发送的修改数据的命令,然后将rdb文件传送给slave,实现一次完全同步
  • 全量复制:slave服务器在接收到master发送的rdb文件后,将其存盘并加载到内存中
  • 增量复制:master将继续收到的客户端命令依次传给slave,完成同步
  • 不过,只要是slave重新连接master,一次全量同步则会被执行

2)常用结构和配置方法

①一主两从

Redis_第5张图片
############# 配置文件准备 #############
mkdir/opt/master-slave
cp /opt/redis-6.2.1/redis.conf /opt/master-slave/
# 复制三份配置文件,并将配置文件名增加-{port}后缀,以便区分
vim /opt/master-slave/redis-{port}.conf
######主配置文件:redis-{port}.conf######
daemonize yes
bind {ip}
# requirepass 123456
dir /opt/master-slave
logfile /opt/master-slave/6379.log
dbfilename dump_6379.rdb
pidfile /var/run/redis_6379.pid
######从配置文件:redis-{port}.conf######
# 端口和日志、持久化文件名同上修改
  • 配置文件方式指定主从关系
######从配置文件:redis-{port}.conf######
# 端口和日志、持久化文件名同上修改
# 指定主机
slave of {ip} {port}
# 主机密码
# masterauth 123456
  • 命令方式指定主从关系
# 分别启动主从服务器
redis-server /opt/master-slave/redis-{port}.conf
# 登录从服务器
redis-cli -h {ip} -p {port} [-a {password}]
# 设置连接master的密码,若有的话
# config set masterauth 123456
# 指定主服务器
slaveof {ip} {port}
  • 验证
# 启动
redis-server /opt/master-slave/redis-{port}.conf
# 登录
redis-cli -h {ip} -p {port} [-a {password}]
# 查看信息
info Replication
# 验证(主写从读)
Redis_第6张图片

②“薪火相传“(slave级联slave)

Redis_第7张图片

③”反客为主“

当master挂掉后,其余slave会原地待命,此时可以手动地从slave中选择一个作为主机,然后再修改其余slave的master指向,挂载到新切换到的maste上。不过这种方式需要手动进行,麻烦易出错。

# 连接到个slave
slaveof no one

④哨兵模式Sentinel Redis - Sentinel-Java充电社

  • 介绍

哨兵模式针对于”反客为主“,无需再手动地进行切换,而是可以自动地监控master是否发生故障。若发送故障,则可从slave中挑选一个作为master,其他的slave会自动指向并同步新的master,实现故障自动转移

  • 原理

sentinel会按照指定的频率给maste发ping请求,看master是否还活着,若master在指定时间内未正常响应sentinel发送的ping请求,sentinel则认为master挂掉了(这存在误判的可能,如网络不畅通)

为了避免误判,通常启动多个sentinel(一般为奇数个)那么可以采用投票法判定若master确实挂了,则会进行故障转移:会从slave中投票选出一个服务器,将其升级为新的主服务器,并让其他slave指向新的主服务器。

当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器

  • 示例:1主3从3哨兵
Redis_第8张图片
############# 主从服务器配置:如上 #############
############# 哨兵配置 #############
# 1.创建配置文件
mkdir /opt/sentinel
touch sentinel-{port}.conf

### 2.sentinel-{port}.conf ###
# 配置文件目录
dir /opt/sentinel/
# 日志文件位置
logfile "./sentinel-{port}.log"
# pid文件
pidfile /var/run/sentinel_{port}.pid
# 是否后台运行
daemonize yes
# 端口
port 26379
# 监控主服务器master的名字:mymaster,IP,port:,最后的数字2表示当Sentinel集群中有2个Sentinel认为master存在故障不可用,则进行自动故障转移
sentinel monitor mymaster {ip} {port} 2
# master响应超时时间(毫秒),Sentinel会向master发送ping来确认master,若在20秒内,ping不通master,则主观认为master不可用
sentinel down-after-milliseconds mymaster 60000
# 故障转移超时时间(毫秒),如果3分钟内没有完成故障转移操作,则视为转移失败
sentinel failover-timeout mymaster 180000
# 故障转移之后,进行新的主从复制,配置项指定了最多有多少个slave对新的master进行同步,那可以理解为1是串行复制,大于1是并行复制
sentinel parallel-syncs mymaster 1
# 指定mymaster主的密码(没有就不指定)
# sentinel auth-pass mymaster 123456

# 3.启动哨兵模式
redis-server sentinel_{port}.conf --sentinel
redis-sentinel sentinel_{port}.conf

# 4.查看哨兵的信息
redis-cli -p {sentinel_port}
info sentinel

# 5.验证故障自动转移(down主,查从)
Redis_第9张图片

当旧的master恢复之后,会自动挂在新的master下面

7.集群

1)集群Cluster

为何使用集群:

  • 单台redis存在容量限制
  • 单台redis并发量存在限制

何为集群:redis集群是对redis的水平扩容,即启动N个redis节点,将整个数据分布存储在这个N个节点中

2)示例:3主3从

一个集群至少有3个主节点,因为新master的选举需要大于半数的集群master节点同意才能选举成功, 如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。

Redis_第10张图片
# 创建工作目录与复制配置文件(3+3=6)
mkdir /opt/cluster
cp /opt/redis-6.2.1/redis.conf /opt/cluster/

# 修改配置文件:master配置文件
include /opt/cluster/redis.conf
daemonize yes
bind {ip}
dir /opt/cluster/
port 6379
dbfilename dump_{port}.rdb
pidfile /var/run/redis_{port}.pid
logfile "./{port}.log"
# 开启集群设置
cluster-enabled yes
# 设置节点配置文件
cluster-config-file node-{port}.conf
# 设置节点失联时间,超过该时间(毫秒),集群自动进行主从切换
cluster-node-timeout 15000

# 修改配置文件:slave配置文件
include /opt/cluster/redis.conf
daemonize yes
bind {ip}
dir /opt/cluster/
port {port}
dbfilename dump_{port}.rdb
pidfile /var/run/redis_{port}.pid
logfile "./{port}.log"
# 开启集群设置
cluster-enabled yes
# 设置节点配置文件
cluster-config-file node-6389.conf
# 设置节点失联时间,超过该时间(毫秒),集群自动进行主从切换
cluster-node-timeout 15000

# 启动mastet、slave
redis-server /opt/cluster/redis-{port}.conf

# 查看6个redis的启动情况
ps -ef | grep redis

# 确保node-{port}.conf文件已正常生成
cd /opt/cluster/
ll

# 将6个节点合成一个集群,1表示1个master配几个slave
redis-cli --cluster create --cluster-replicas 1 {ip1:port1} {ip1:port1} {ip2:port2} {ip3:port3} {ip4:port4} {ip5:port5} {ip6:port6}  

# 连接集群节点:连接集群中6个节点中任何一个节点就可以,注意-c参数
redis-cli -c -h {ip} -p {port}

# 查看集群信息
cluster nodes

3)槽slots

Redis集群内部划分了16384个slots(插槽),合并的时候,会将每个slots映射到一个master。如其中一个映射关系:

redis主节点 槽位范围
master1(端口:6379) [0-5460],插槽的位置从0开始的,0表示第1个插槽
master2(端口:6380) [5460-10922]
master3(端口:6381) [10923-16383]
slave1,slave2,slave3 从节点没有槽位,slave是用来对master做替补的
  • 数据库中的每个key都属于16384个slots中的其中1个,当通过key读写数据的时候,redis需要先根据 key计算出key对应的slots,然后根据slots和master的映射关系找到对应的redis节点,key对应的数据 就在这个节点上面。

    集群中使用公式计算key属于哪个槽:$ CRC16(key)%16384 $

  • 在 redis-cli 每次录入、查询键值,redis都会计算key对应的插槽,如果不是当前redis节点的插槽, redis会报错,并告知应前往的redis实例地址和端口。使用redis-cli客户端提供了-c参数可以解决这个问题,表示以集群方式执行,执行命令的时候当前节点处理不了的时候,会自动将请求重定向到目标节点

  • 不在一个slot下面,不能使用mget、mset等多键操作,可以通过{}来定义组的概念,从而使key中{}内相同的键值放到一个slot中去

# 错误操作
mset k1 v1 k2 v2
# (error) CROSSSLOT Keys in request don't hash to the same slot
# 正确操作
mset k1{g1} v1 k2{g1} v2 k3{g1} v3
# OK
  • slot相关命令
# 计算key对应的slot
cluster keyslot <key>
# 获取slot槽位中key的个数
cluster coutkeysinslot <slot>
# 返回count个slot槽中的键
cluster getkeysinslot <slot> <count> 

4)故障修复

如果主节点下线,从节点要等15s可以提升为主节点。如果一段插槽的主从都宕机了,redis服务是否还能提供服务由配置文件中的cluster-require-full-coverage决定:

  • yes(默认值):整个集群都都无法提供服务了
  • no:宕机的这部分槽位数据全部不能使用,其他槽位正常

8.应用问题与解决

1.缓存穿透

1)问题描述

当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没 有就去db中查询,db中如果有就会将其丢到缓存中,但是有些key对应更多数据在db中并不存在,每次 针对此次key的请求从缓存中取不到,请求都会压到db,从而可能压垮db。

比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用大量此类攻击可能 压垮数据库。

2)解决方案

①对空值缓存:如果一个查询返回的数据为空(不管数据库是否存在),我们仍然把这个结果(null)进行缓存,给其 设置一个很短的过期时间,最长不超过五分钟

②设置可访问的名单(白名单):使用redis中的bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次范文和 bitmap里面的id进行比较,如果访问的id不在bitmaps里面,则进行拦截,不允许访问

③采用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截 掉,从而避免了对底层存储系统的查询压力。布隆过滤器(Bloom Filter)是1970年有布隆提出的,它实际上是一个很长的二进制向量(位图)和一 系列随机映射函数(哈希函数)。 布隆过滤器可以用于检测一个元素是否在一个集合中,它的优点是空间效率和查询的世界都远远超过一 般的算法,缺点是有一定的误识别率和删除困难。

④进行实时监控:当发现redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名 单限制对其提供服务(比如:IP黑名单)

2.缓存击穿

1)问题描述

redis中某个热点key(访问量很高的key)过期,此时大量请求同时过来,发现缓存中没有命中,这些请 求都打到db上了,导致db压力瞬时大增,可能会打垮db,这种情况称为缓存击穿。

2)解决方案

①预先设置热门数据,适时调整过期时间:在redis高峰之前,把一些热门数据提前存入到redis里面,对缓存中的这些热门数据进行监控,实时调 整过期时间

②使用锁

缓存中拿不到数据的时候,此时不是立即去db中查询,而是去获取分布式锁(比如redis中的setnx), 拿到锁再去db中load数据;没有拿到锁的线程休眠一段时间再重试整个获取数据的方法

3.缓存雪崩

1)问题描述

key对应的数据存在,但是极短时间内有大量的key集中过期,此时若有大量的并发请求过来,发现缓存 没有数据,大量的请求就会落到db上去加载数据,会将db击垮,导致服务奔溃。

缓存雪崩与缓存击穿的区别在于:前者是大量的key集中过期,而后者是某个热点key过期

2)解决方案

①构建多级缓存:nginx缓存+redis缓存+其他缓存(ehcache等)

②使用锁或队列:用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发 请求落到底层存储系统上,但不适用高并发情况

③监控缓存过期,提前更新:监控缓存,发下缓存快过期了,提前对缓存进行更新

④将缓存失效时间分散开:在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样缓存的过期时间重复率 就会降低,就很难引发集体失效的事件

4.分布式锁

1)问题描述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多 进程且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分 布式锁要解决的问题。

2)分布式锁的主流实现方案

①基于数据库实现分布式锁 ②基于缓存(redis等)③基于zookeeper

每一种分布式锁解决方案都有各自的优缺点:redis性能最高;zookeeper可靠性最高

3)使用redis实现分布式锁

①上锁

执行set key value NX PX有效期(毫秒) 命令,返回ok表示执行成功,则获取锁成功, 多个客户端并发执行此命令的时候,redis可确保只有一个可以执行成功。

# 当key不存在的时候,设置其值为value,且同时设置其有效期
set key value NX PX 有效期(毫秒)
Redis_第11张图片

②常见问题

  • 过期时间
    • 为何要设置过期时间:客户端获取锁后,由于系统问题,如系统宕机了,会导致锁无法释放,其他客户端就无法或锁了,所以 需要给锁指定一个使用期限。
    • 过期时间过短:有效期可能不够业务方使用,这种情况客户端需要实现续命的功能,可以解决这 个问题。
  • 解决误删问题
    • 问题描述:如线程A获取锁的时候,设置的有效期是10秒,但是执行业务的时候,A程序突然卡主了超过了10秒, 此时这个锁就可能被其他线程拿到,比如被线程B拿到了,然后A从卡顿中恢复了,继续执行业务,业务执行完毕之后,去执行了释放锁的操作,此时A会执行del命令,此时就出现了锁的误删,导致的结果就是把B持有的锁给释放了
    • 一般解决方法:获取锁的之前,生成一个全局唯一id,将这个id也丢到key对应的value中,释放锁之前,从redis中将这 个id拿出来和本地的比较一下,看看是不是自己的id,如果是的再执行del释放锁的操作
    • 解决方法:上面的一般解决方法,由于未保证原子性因而还可能出现误删的情况,此时可以使用具有原子性的Lua脚本(Redis2.6以上支持)完成上述操作

③小结

  • 互斥性:在任意时刻只能有一个客户端能够持有锁
  • 不糊发生死锁:即使有一个客户端在持有锁期间崩溃而没有释放锁,也能够保证后续其他客户端能够加锁
  • 解锁还需寄铃人:加锁和解锁必须是同一个客户端,客户端不能把别人的锁给解了
  • 加锁和解锁必须有原子性

9.Java整合Redis

1.Jedis

redis/jedis: Redis Java client (github.com) Jedis的使用及配置优化 - 简书 Jedis常用API整理-CSDN博客

1)引入依赖

<dependency>
	<groupId>redis.clientsgroupId>
	<artifactId>jedisartifactId>
	<version>2.9.3version>
dependency>

2)基本用法

public class JedisApp {
    public static void main(String[] args) {
        // Jedis客户端创建
        Jedis jedis = new Jedis("localhost", 6379);
        // 测试连接
        String ping = jedis.ping();
        System.out.println(ping);
        // String类型测试
        jedis.set("name","LinCat");
        System.out.println(jedis.get("name"));
        System.out.println(jedis.ttl("name"));
        // list类型测试
        jedis.rpush("Courses","Deep Learning","Distributed System","Big data");
        List<String> courses = jedis.lrange("Courses",0,-1);
        for(String course:courses)  System.out.println(course);
        // set、hash、zSet类型测试
        // 订阅消息
        jedis.subscribe(new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                super.onMessage(channel, message);
                System.out.println(channel + ":" + message);
                try {
                    TimeUnit.SECONDS.sleep(15);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"channel");
        // 发布消息
        jedis.publish("channel","hello,LinCat");
    }
}

2.SpringBoot整合Jedis

Spring Data Redis SpringBoot揭密:spring-boot-starter-data-redis - 掘金

1)引入依赖


<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-data-redisartifactId>
	<version>3.0.0version>
dependency>

2)配置文件

spring:
  redis:
    host: localhost
    port: 6379
    # password: 123456
    # 连接超时时间(毫秒)
    timeout: 60000
    # 指定使用哪一个分片
    database: 0

3)基本操作

@RestController
@RequestMapping("/redis")
public class CRedisController {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @RequestMapping("/opts")
    public void opts(){
        /**
         * 显示对不同数据类型可以执行的操作
         */
        System.out.println(this.redisTemplate.opsForValue());
        System.out.println(this.redisTemplate.opsForList());
        // ...
    }

    @RequestMapping("info")
    public String info(){
        /**
         * 查看redis机器信息
         */
        Object obj = this.redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.execute("info");
            }
        });
        return obj.toString();
    }

    @RequestMapping("/stringTest")
    public String StringTest(){
        /**
         * 操作String
         */
        this.redisTemplate.delete("name");
        this.redisTemplate.opsForValue().set("name","LinCat");
        String name = this.redisTemplate.opsForValue().get("name");
        return name;
    }

    @RequestMapping("/listTest")
    public List<String> listTest(){
        /**
         * 操作list
         */
        this.redisTemplate.delete("Courses");
        this.redisTemplate.opsForList().rightPushAll("Courses","Deep Learning",
                "Distributed System","Big data");
        List<String> courses = this.redisTemplate.opsForList().range("names", 0, -1);
        return courses;
    }

    /** 操作其他数据类型... **/
}

4)SpringBoot整合哨兵模式

配置从机

spring:
  redis:
    host: localhost
    port: 6379
    # password: 123456
    # 连接超时时间(毫秒)
    timeout: 60000
    # 指定使用哪一个分片
    database: 0
    # 配置从机信息
    sentinel:
      # redis sentinel主服务名称,来源于:sentinel配置文件中sentinel monitor后面跟的那个名称
      master: mymaster
      # sentinel节点列表(host:port),多个之间用逗号隔开
      nodes: 192.168.200.129:26379,192.168.200.129:26380,192.168.200.129:26381
      # sentinel密码
      # password: 123456

5)SpringBoot整合集群

spring:
  redis:
    host: localhost
    port: 6379
    # password: 123456
    # 连接超时时间(毫秒)
    timeout: 60000
    # 指定使用哪一个分片
    database: 0
    cluster:
      # 集群节点(host:port),多个之间用逗号隔开
      nodes: 192.168.200.129:6379,192.168.200.129:6380,192.168.200.129:6381,192.168.200.129:6389,192.168.200.129:6390,192.168.200.129:6391
      

你可能感兴趣的:(redis)