Redis

Linux安装

【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili

Keys

序号 命令及描述
1 DEL key 该命令用于在 key 存在时删除 key。
2 DUMP key 序列化给定 key ,并返回被序列化的值。
3 EXISTS key 检查给定 key 是否存在。
4 EXPIRE key seconds 为给定 key 设置过期时间,以秒计。
5 EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
6 PEXPIRE key milliseconds 设置 key 的过期时间以毫秒计。
7 PEXPIREAT key milliseconds-timestamp 设置 key 过期时间的时间戳(unix timestamp) 以毫秒计
8 KEYS pattern 查找所有符合给定模式( pattern)的 key 。
9 MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。
10 PERSIST key 移除 key 的过期时间,key 将持久保持。
11 PTTL key 以毫秒为单位返回 key 的剩余的过期时间。
12 TTL key 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。
13 RANDOMKEY 从当前数据库中随机返回一个 key 。
14 RENAME key newkey 修改 key 的名称
15 RENAMENX key newkey 仅当 newkey 不存在时,将 key 改名为 newkey 。
16 SCAN cursor [MATCH pattern …] [COUNT count] 迭代数据库中的数据库键。
17 TYPE key 返回 key 所储存的值的类型。

String

序号 命令及描述
1 SET key value 设置指定 key 的值
2 GET key 获取指定 key 的值。
3 GETRANGE key start end 返回 key 中字符串值的子字符
4 GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
6 MGET key1 [key2..] 获取所有(一个或多个)给定 key 的值。
7 SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
8 SETEX key seconds value 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。
9 SETNX key value 只有在 key 不存在时设置 key 的值。
10 SETRANGE key offset value 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。
11 STRLEN key 返回 key 所储存的字符串值的长度。
12 MSET key value [key value …] 同时设置一个或多个 key-value 对。
13 MSETNX key value [key value …] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
14 PSETEX key milliseconds value 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。
15 INCR key 将 key 中储存的数字值增一。
16 INCRBY key increment 将 key 所储存的值加上给定的增量值(increment) 。
17 INCRBYFLOAT key increment 将 key 所储存的值加上给定的浮点增量值(increment) 。
18 DECR key 将 key 中储存的数字值减一。
19 DECRBY key decrement key 所储存的值减去给定的减量值(decrement) 。
20 APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。
  • GETRANGE key start end获取key[start,end],end/start为负数时代表倒数第|end|/|start|个字符。当start值大于end时返回空字符串!!

List

Redis的List与常规说的列表有所不同,一般来说列表是单口的,即.append()只能在列表尾添加元素。但是Redis的List是双口的,能在列表首尾添加元素。而且元素的取存是和堆栈一致的。

序号 命令及描述
1 BLPOP key1 [key2 …] timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
2 BRPOP key1 [key2 …] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
3 BRPOPLPUSH source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
4 LINDEX key index 通过索引获取列表中的元素
5 LINSERT key BEFORE|AFTER pivot value 在列表的元素前或者后插入元素
6 LLEN key 获取列表长度
7 LPOP key 移出并获取列表的第一个元素
8 LPUSH key value1 [value2 …] 将一个或多个值插入到列表头部
9 LPUSHX key value 将一个值插入到已存在的列表头部
10 LRANGE key start stop 获取列表指定范围内的元素
11 LREM key count value 移除列表count个相同元素
12 LSET key index value 通过索引设置列表元素的值
13 LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
14 RPOP key 移除列表的最后一个元素,返回值为移除的元素。
15 RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
16 RPUSH key value1 [value2 …] 在列表中添加一个或多个值
17 RPUSHX key value 为已存在的列表添加值

LPUSH相当于堆栈的FILO原则,RPUSH相当于队列的FIFO原则

AFTER是队列右端插入元素,BEFORE是往队列左端插入元素

Set

和Java的Set一样,Set是一个无序不重复集合,因而Set不能通过下标指定获取元素,但可以用作随机数集合。

序号 命令及描述
1 SADD key member1 [member2 …] 向集合添加一个或多个成员
2 SCARD key 获取集合的成员数
3 SDIFF key1 [key2 …] 返回第一个集合与其他集合之间的差异。
4 SDIFFSTORE destination key1 [key2 …] 返回给定所有集合的差集并存储在 destination 中
5 SINTER key1 [key2 …] 返回给定所有集合的交集
6 SINTERSTORE destination key1 [key2 …] 返回给定所有集合的交集并存储在 destination 中
7 SISMEMBER key member 判断 member 元素是否是集合 key 的成员
8 SMEMBERS key 返回集合中的所有成员
9 SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
10 SPOP key 移除并返回集合中的一个随机元素
11 SRANDMEMBER key [count …] 返回集合中一个或多个随机数
12 SREM key member1 [member2 …] 移除集合中一个或多个成员
13 SUNION key1 [key2 …] 返回所有给定集合的并集
14 SUNIONSTORE destination key1 [key2 …] 所有给定集合的并集存储在 destination 集合中
15 SSCAN key cursor [MATCH pattern …] [COUNT count] 迭代集合中的元素

Hash

序号 命令及描述
1 HDEL key field1 [field2 …] 删除一个或多个哈希表字段
2 HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
4 HGETALL key 获取在哈希表中指定 key 的所有字段和值
5 HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。
6 HINCRBYFLOAT key field increment 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
7 HKEYS key 获取所有哈希表中的字段
8 HLEN key 获取哈希表中字段的数量
9 HGET key field1 [field2 …] 获取所有给定字段的值
10 HSET key field1 value1 [field2 value2 …] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
12 HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。
13 HVALS key 获取哈希表中所有值。
14 HSCAN key cursor [MATCH pattern …] [COUNT count] 迭代哈希表中的键值对。
  • HMGET和HMSET在Redis 4.0.0后被官方废弃,其功能和HSET,HGET一致。

  • HGETALL可以获取key-value,HKEYS只能获取key,HVALS只能获取value

Zset

Zset相比于Set,是个有序的无重复集合,Zset的主要方法用来排列集合数据

Zset存储的数据结构也与Set不一样,类似于value-key的结构,这样就可以通过key来定向获取分数了。

序号 命令及描述
1 ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
2 ZCARD key 获取有序集合的成员数
3 ZCOUNT key min max 计算在有序集合中指定区间分数的成员数
4 ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
5 ZINTERSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中
6 ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量
7 ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]通过索引区间返回有序集合指定区间内的成员
10 ZRANK key member 返回有序集合中指定成员的索引
11 ZREM key member [member …] 移除有序集合中的一个或多个成员
17 ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
18 ZSCORE key member 返回有序集中,成员的分数值
19 ZUNIONSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的并集,并存储在新的 key 中
20 ZSCAN key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值)
21 ZRANDMEMBER key [count [WITHSCORES]] 随机获取集合中count个member
  • Zset会自动对内部的数据进行递增的排序

  • ZRANGE是个功能齐全的方法,能够替代之前的ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZRANGEBYLEX and ZREVRANGEBYLEX.

  • ZRANGE key min max是以下标为截取范围如ZRANGE key 0 -1截取第一个到倒数第一个。

  • ZRANGE key min max BYSCORE是以分数为截取范围,如ZRANGE key -inf +inf BYSCORE截取负无穷到正无穷

  • ZRANGE key min max BYLEX是以字典顺序为截取范围,且只以member在字典的顺序为排序(此模式下要求最好所有member的分数都要一致,否则返回的结果不可预测)

    Zset是以分数排序,因而a排在最后

    因为BYLEX只看member在字典的顺序,而a排在最后面违反了Zset递增排序的规则,这样BYLEX查询就会出错。

    对于ZADD myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g而言,ZRANGE myzset - + BYLEX截取所有member。ZRANGE myset (a [d BYLEX返回b、c、d

  • ZRANGE key max min REV会以降序返回,但注意范围是max min。

  • ZRANGE key min max WITHSCORES会将member和score一起返回,默认只返回member。

  • ZCOUNT key min max是基于分数为截取范围,ZLEXCOUNT key min max才是基于字典顺序为截取范围。

Geospatial

序号 命令及描述
1 GEOADD key longitude latitude member [longitude latitude member …] 添加地理位置
2 GEOPOS key member [member …] 获取地点的经纬
3 ZREM key member [member …] Geo没有专门的删除方法,但Geo也是一个Zset。
4 GEODIST key member1 member2 [m|km|ft|mi] 测量两地点之间的直线距离
5 GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC|DESC] [STORE key] [STOREDIST key] 获取离特定点radius距离内的所有地点,并按从近到远排列输出
6 [GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi] [BYBOX width height m|km|ft|mi] [ASC|DESC] COUNT count [ANY] ] [WITHCOORD] [WITHDIST] 寻找count个以member或指定坐标为中心的圆形/方形范围内的地点。
  • GEOADD china:city 121.47 31.23 shanghai 116.40 39.90 beijing 106.50 29.53 chongqing 114.05 22.52 shenzhen 120.16 30.24 hangzhou

  • 有效经度为 -180 到 180 度。有效纬度是从 -85.05112878 到 85.05112878 度。

  • GEORADIUS在Redis 6.2.0已经被弃用,代替的是 GEOSEARCHGEOSEARCHSTORE

  • FROMMEMBER:使用给定的<成员>在已排序集合中的位置。
    FROMLONLAT:使用给定的<经度>和<纬度>位置。
    BYRADIUS:类似于GEORADIUS,根据给定的<radius>搜索圆形区域内。
    BYBOX:在轴对齐的矩形内搜索,由<height><width>决定。
    COUNT:在返回结果中截取前COUNT个元素,如果开启ANY选项,直到找到足够的匹配项就直接返回。否则会全部找完再进行排序。
    WITHCOORD:将地点的经纬同时返回
    WITHDIST:将地点与中心点的距离也返回,距离单位与搜寻范围的距离单位一致

Hyperloglog

序号 命令及描述
1 PFADD key element [element …] 添加一组元素
2 PFCOUNT key [key …] 返回所有key集合交集的基数
3 PFMERGE destkey sourcekey [sourcekey …] 合并sourcekey并存到destkey
  • PFADD如果添加了已有的元素,重复的元素将被阻止添加。
  • PFCOUNT计算集合的基数(集合中不重复的元素个数),当PFCOUNT用单个键调用时,因为PFCOUNT使用缓存来记住之前计算的基数,而基数很少改变,故而计算时间很短。当使用多个键调用PFCOUNT时,会执行HyperLogLogs的动态合并,而且无法缓存合并的基数,故而计算时间较长。

Bitmap

序号 命令及描述
1 SETBIT key offset value 在offset偏移处设置0/1
2 GETBIT key offset 获取offset的bit位
3 BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
4 BITCOUNT key [start end] 统计bit位为1的位数
5 BITOP AND|OR|XOR destkey key [key …] 将所有key进行位运算并存储结果到destkey
BITOP NOT destkey srckey
  • Bitmap并不是一个特殊的数据结构,可以看作是字符串类型。因而bitmap也能使用GET、SET方法。
  • SETBIT设置了一个高bit位的值后,Bitmap会自动增长以确保能在offset处保持位,未被设置的位则默认为0。offset参数要求大于或等于0,并且小于2^32^(这将Bitmap限制为512MB)

Redis事务

Redis事物本质是一组命令的集合,在事务的执行过程中会按照顺序执行。Redis事务是一次性、顺序性、排他性的。

但注意,Redis事务没有隔离级别的概念,而Redis命令是保持原子性的,但事务本身不保证原子性!

事务执行

  1. MULTI(开启事务)

  2. SET、GET … (命令入队)

  3. EXEC (执行事务)

事务出现编译异常

Redis命令如果出现了语法错误,当其尝试入队时Redis会提示错误。最后运行时整个事务都会被阻止。

运行时异常

Redis命令如果操作了不得当数据,如自增字符串,用Hash命令操作List、String等,执行事务时正常命令可以照样执行,错误命令则抛出异常。

Jedis

通过Jedis连接远程服务器的Redis:

建立maven项目,导入依赖

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>

创立代码文件:

1
2
3
4
5
6
7
8
9
10
package com.levi;

import redis.clients.jedis.Jedis;

public class TestJedis {
public static void main(String[] args) {
Jedis jedis = new Jedis("47.101.160.130", 6379);
System.out.println(jedis.ping());
}
}

改变Redis配置(备份在/usr/local/bin/leviconfig/redis.conf),允许远程连接:

注释掉bind,protected-mode为no

img

防火墙开启6379端口:

1
2
3
4
firewall-cmd --zone=public --add-port=6379/tcp --permanent
#重新加载并查看开放的端口号
firewall-cmd --reload
firewall-cmd --permanent --zone=public --list-ports

阿里云开放安全组6379端口

以上完成后执行Java代码即可看到PONG的输出。

Redis持久化

Redis数据库是存在于内存中的,具有断电即失的特点,因而Redis持久化数据很重要

RDB(Redis DataBase)

通过拍摄快照的方式实现持久化,将某个时间的内存数据存储在一个rdb文件中,在redis服务重新启动的时候加载文件中的数据

Redis会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件中,带持久化过程结束了,再用该临时文件替换上次持久化好的文件。整个持久化的过程中,主进程不进行任何IO操作,创建子进程后主进程仍然可以响应Client请求。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是很敏感的话,RDB比AOF方式更为有效。

RDB的缺点就是最后一次持久化后的数据可能会丢失

Redis.conf

以下是Redis配置文件关于RDB的默认配置值。

RDB触发条件

1、 满足save 900 1 的条件

2、 执行了bgasve/save命令,bgsave不会阻塞主进程,而save会

3、 shutdown退出Redis/flush清除数据

注:FLUSHALL生成的rdb/aof文件是能储存flush之前的数据的:Redis的flushall/flushdb误操作 - 小家电维修 - 博客园 (cnblogs.com)

RDB恢复快照

1
2
3
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/bin"

只要dump.rdb保留在/usr/local/bin(默认在redis-server同级目录),重新启动redis就能重载备份数据。

FLUSHALL后恢复RDB数据

Redis使用rdb文件恢复数据 - 天宇轩-王 - 博客园 (cnblogs.com)

AOF(Append Only File)

AOF文件能够将我们的操作记录下来,在redis启动的时候会顺序执行记录的写命令。这种方式明显就不适合大量数据的存储了,但能很好地防止Flush的误操作恢复备份。

Redis.conf

AOF触发条件

appendfsync默认是每秒触发,即每秒记录一次写操作。

AOF修复

AOF可能因为电脑宕机导致文件破损,使用redis-check-aof --fix就可以修复AOF文件,修复方式就是删去出错的写操作。如果AOF没有被修复,那么Redis是无法开启的

FLUSH后恢复AOF数据

Redis AOF之执行flushdb或flushall之后的后悔药_小楼一夜听春雨,深巷明朝卖杏花-CSDN博客

Redis集群

环境搭建

在同一台服务器上搭建一主二从的拟集群环境,建立三分Redis配置文件,分别为redis79.conf,redis80.conf,redis81.conf

【狂神说Java】Redis集群环境搭建

搭建好后进入redis,会发现每一台redis都是主节点。因为默认情况下每个节点都是主节点

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:a3d4f6b6132911f4933991b2f6b3b69cc5aeb40a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

选择一个作为从机通过SLAVEOF 127.0.0.1 6379就能指定主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:28
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:32cde58052534f54c17c79fe0728a522c19e1377
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

实际情况下要通过配置文件设置主从关系,更改从机配置文件的relicaof <masterip> <masterport>masterauth <master-password>

主从复制

注意

  • 整个集群中只有主机有读写功能,而从机没有写功能。从机共享主机资源,可以备份数据。

  • 主机宕机且不配置哨兵模式时,如果主机宕机退出,从机依然依附原主机。但此时整个集群都不能做写操作!!

  • 从机宕机后重连。除非配置文件中设置了主从关系,否则该机器脱离集群!!当然也可通过SLAVEOF进入集群。

复制原理

从机连接到集群后会像主机发送sync同步请求,主机接收后启动后台存盘进程,同时从机所有接收到的用于修改数据集的命令,在后台进程执行完毕后,主机同步所有数据文件到从机。

==全量复制==:从机接收到数据文件后,将其存盘并加载至内存

==增量复制==:后续主机会将新的数据依次传递给从机

只要从机连接到集群,就会自动完成一次全量复制

哨兵

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

单哨兵

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

用文字描述一下多哨兵故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

多哨兵

单哨兵

创建哨兵配置文件sentinel.conf:

1
2
3
4
5
6
7
# 禁止保护模式(多服务器)
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,
#192.168.11.128代表监控的主服务器,6379代表端口,1代表只有一个或一个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 127.0.0.1 6379 1
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
sentinel auth-pass mymaster 123456

启动哨兵,哨兵启动成功后会更新sentinel.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@Levi bin]$ redis-sentinel sentinel.conf 
2771847:X 15 Sep 2021 22:04:41.712 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2771847:X 15 Sep 2021 22:04:41.712 # Redis version=6.2.5, bits=64, commit=00000000, modified=0, pid=2771847, just started
2771847:X 15 Sep 2021 22:04:41.712 # Configuration loaded
2771847:X 15 Sep 2021 22:04:41.713 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.5 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 2771847
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'

2771847:X 15 Sep 2021 22:04:41.717 # Sentinel ID is 752d9dbfe066f2ec0206227c0be60f8500478a50
2771847:X 15 Sep 2021 22:04:41.717 # +monitor master mymaster 127.0.0.1 6379 quorum 1
2771847:X 15 Sep 2021 22:04:41.717 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 #发现了两个从机
2771847:X 15 Sep 2021 22:04:41.720 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379

宕机主机,哨兵确认宕机后会投票选取一个从机为新的主机,相关日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2771847:X 15 Sep 2021 22:09:49.686 # +sdown master mymaster 127.0.0.1 6379									##察觉到主机宕机
2771847:X 15 Sep 2021 22:09:49.686 # +odown master mymaster 127.0.0.1 6379 #quorum 1/1 ##确认宕机
2771847:X 15 Sep 2021 22:09:49.686 # +new-epoch 1
2771847:X 15 Sep 2021 22:09:49.686 # +try-failover master mymaster 127.0.0.1 6379 ##开始选举
2771847:X 15 Sep 2021 22:09:49.689 # +vote-for-leader 752d9dbfe066f2ec0206227c0be60f8500478a50 1 ##752d9db号哨兵发起投票
2771847:X 15 Sep 2021 22:09:49.689 # +elected-leader master mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:49.689 # +failover-state-select-slave master mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:49.780 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 ##投票给127.0.0.1:6381
2771847:X 15 Sep 2021 22:09:49.780 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:49.838 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:49.991 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379 ##确认127.0.0.1:6381为新主机
2771847:X 15 Sep 2021 22:09:49.991 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:50.059 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:51.059 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:51.059 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:51.130 # +failover-end master mymaster 127.0.0.1 6379
2771847:X 15 Sep 2021 22:09:51.130 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6381 ##更改127.0.0.1:6381配置,成为新主机
2771847:X 15 Sep 2021 22:09:51.130 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381 ##更改从机配置文件
2771847:X 15 Sep 2021 22:09:51.130 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
2771847:X 15 Sep 2021 22:10:21.132 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381

即使原主机回来,也只能成为新主机的从机。

哨兵配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# Example sentinel.conf  

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 哨兵sentinel的工作目录
dir /tmp

# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd

# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,
#这个数字越小,完成failover所需的时间就越长,
#但是如果这个数字越大,就意味着越多的slave因为failover而不可用。
#可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1

# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION

#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。

#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh

# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

Redis击穿和雪崩

缓存穿透

绕过缓存直接攻击数据库

对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

==解决方案:==

  • 缓存空对象

如果数据库中找不到,就会返回一个空值储存到缓存中,并设置过期时间。在有效期内,所有相同key的请求都能在缓存中找到值。

  • 布隆过滤器

布隆过滤器对可能的参数以hash形式储存,其他不合理的请求会被丢弃,从而避免直接对数据库的施压。

==隐患:==

但如果大量不同的key都找不到数据,就会导致缓存中存有大量无效的空值,从而降低性能。

缓存击穿

当某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

==解决方案:==

  • 设置key永不过期

这种方式可以说是最可靠的,最安全的但是占空间,内存消耗大,并且不能保持数据最新 这个需要根据具体的业务逻辑来做。但也可以隔段时间便去数据库更新数据,此时要求不会给数据库很大的压力。

  • 使用互斥锁

就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用某些一定能成功返回值的操作(比如Redis的SETNX)去set一个mutex key(一个特定的key,当成竞争资源),当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法

站在用户层面,但key失效时用户获取的就是空页面。因此只需要多刷新几次,等待缓存恢复正常就能获取到正常页面了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作前程序出误,导致锁一直不能被释放
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //说明此时获得了锁
value = db.get(key);
redis.set(key, value, expire_secs); //设置key的存在时间,避免出现key永不过期的现象
redis.del(key_mutex);
} else { //等待一段时间后,持锁线程已经完成load db,故而重试get即可。
sleep(50);
get(key); //重试
}
else {
return value;
}
}

redis.set(key, value, expire_secs)是有讲究的,如果在SETNX和EXPIRE分开操作,但两者之间程序又发生了错误,当前锁又无法释放,key成为了永不过期。所以根本原因还是需要一个原子的操作,在获得锁的同时能够同时设置锁的过期时间。

缓存雪崩

对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

缓存失效,大量请求攻击数据库

==解决方案:==

  • Redis高可用

有预知地在流量集中爆发的时间段前,扩大缓存集群(如暂停其他服务)来应付集中的请求,以此避免全盘崩溃

  • ehcache 缓存 + hystrix 限流&降级

在缓存失效后,通过加锁或是限制队列长度的方式控制流量,避免数据库被直接打死。

  • 数据预热

有预知地让缓存去数据库访问一遍可能会被集中访问的key,在大并发访问之前手动触发加载缓存的预热key。

Redis应用

Redis应用场景

  • 对于具有时效性的业务功能如验证码,订单有效期
  • redis分布式集群系统可以用作Session共享
  • redis的zset类型可用作实时排行榜
  • redis的setnx用作分布式锁
  • 分布式缓存

Redis过期策略及内存淘汰

Redis对于过期数据或溢出内存后进行淘汰数据的策略可概括为定期删除+惰性删除

为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除和惰性删除

定期删除,redis默认每个100ms随机抽取key进行检查,如果key过期则删除。但只采用定期删除,可能会导致很多过期key无法被检查到。

惰性删除,当获取某一key时,如果该key设置了过期时间,再检查是否过期,过期了才会删除。

但如果有一过期key没有被检查到,也没有被使用上,即绕开了定期删除与惰性删除的机制。这种key就会一直堆积在内存中,就需要引入内存淘汰机制

内存淘汰机制

redis.conf中可以配置内存淘汰机制,当内存不足以纳入新的数据时就会触发机制:

1
maxmemory-policy allkeys-lru
  • allkeys-lru:在键空间中,优先淘汰最近最少使用的key
  • noeviction:写入新数据时会报错。
  • allkeys-random:在键空间中,随机移除某一key
  • volatile-lru:在设置了过期时间的键空间中,优先淘汰最近最少使用的key
  • volatile-random:在设置了过期时间的键空间中,随机移除某一key
  • volatile-ttl:在设置了过期时间的键空间中,优先淘汰TTL设置得更早的key

Redis与数据库一致性问题

当数据库和缓存双写,必定会存在不一致的问题。为了解决这一问题,我们可以采用双删+TTL失效来实现,但这也只能保证数据的最终一致性。如果对数据有强一致性的要求,就不能设置缓存!!!

数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:

  1. 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中依然为脏数据。
  2. 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

为了解决一致性问题,普遍有以下四种方式(为简述方便,对于每一种方法的两步分别以1,2标识,如写进程A的两步分别为A1,A2):

  1. 先更新数据库,再更新缓存
  2. 先更新缓存,再更新数据库
  3. 先删除缓存,再更新数据库
  4. 先更新数据库,再删除缓存

==先更新数据库,再更新缓存==

对于写进程A,B,执行顺序为A1-B1-B2-A1,这样缓存中的数据还是A的数据,数据出现不一致。

==先更新缓存,再更新数据库==

对于写进程A,A2在A1后执行失败,数据依旧不一致。

其实,通过更新缓存来同步数据库数据的两种方式都有以下两个问题:

  1. 对于写操作频繁的场景,缓存的数据时刻都在更新,浪费性能
  2. 如果真正需要写入缓存的数据是要进入数据库后再演算出来的,那么先更新缓存,所更新的数据也是错误的。

那么,通过懒加载的方式(删除缓存)同步数据库数据更佳


懒加载

当数据库中的数据更新/删除时选择删除缓存中的数据,删除后不更新新数据到缓存中,直到查询的时候没命中缓存,访问数据库后再添加至缓存

Redis懒加载缓存.png

在懒加载的概念下,我们讨论后两种方式:先删除缓存,再更新数据库/先更新数据库,再删除缓存

==先删除缓存,再更新数据库==

  1. 线程 A 要更新 X = 2(数据库中 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

缓存中依然留的是旧值。这还是一个数据库出现的问题,如果在主从库的环境下:

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1)

为了解决以上两个问题,业界给出了延迟双删的答案:

延迟双删

1)先删除缓存;

2)再写数据库;

3)触发异步写入串行化MQ(也可以采取一种key+version的分布式锁);

4)MQ接受再次删除缓存。

两次删除的目的是防止在1)到 2)的过程中读操作访问缓存,从而把还未更新的数据由存到了缓存中去。延迟删除就保证了此后缓存中的数据是最新的。

但是!!双删依旧存在问题!!

1、A删除缓存

2、B查询数据库获取旧值

3、B更新了缓存

4、A更新数据库

5、A延时删缓存

这种情况下,1-3步会让删除的旧缓存重新回来了,那么先删除缓存就没有任何意义。

此外!!如果步骤3要晚于步骤5,以后的读操作读的还是旧缓存!!!(虽然可以加长延时长度来避免)


==先更新数据库,再删除缓存==

综上,该方式能应付大多并发请求。为了防止删除缓存程序宕机,可以设置缓存有效时间

但这样更新数据库到删除缓存的这段时间内,缓存和数据库无法保证一致性。

串行化

对于==先删除缓存,再更新数据库==,其问题就是没能保证读应该在写之后发生,为此我们可以引用串行MQ来保证读写的顺序执行。

  1. 先删缓存,将更新数据库的操作放进有序队列中
  2. 如果缓存查不到,访问数据库和更新缓存的操作都进入有序队列

我们再考虑以下场景:

  1. 线程 A 先删除缓存
  2. 线程 A 要更新 X = 2(数据库中 X = 1),更新数据库请求进入MQ
  3. 线程 B 读缓存,发现不存在,读取数据库请求进入MQ
  4. MQ消费到更新数据库请求,线程 A 将新值写入数据库(X = 2)
  5. MQ消费到读取数据库请求,线程 B 读取新值(X = 2)
  6. MQ消费到更新缓存请求,线程 B更新缓存

可以优化的一点是,当有多个连续更新缓存请求在MQ中,只需要保留最新的更新请求即可。

但该方案依旧存在问题!!!

  1. 如果数据库更新频繁,缓存一直没有值,这就会导致大量读取数据库请求积压在MQ末端中,如果放行这些请求,数据库的压力会很大。
  2. 串行化虽然能保证不会出现数据不一致的问题,但是高并发更新操作场景下,读操作被推迟,严重降低系统吞吐量,影响客户体验。

重发

==消息队列==

无论是先还是后操作数据库,两步没有全走完都会出现不一致的后果,为了保证请求能确保被完全执行,我们可以引入消息队列,原因有三:

  1. 重发操作不能由业务层发起,这会严重干扰业务进行。同时如果执行失败的线程中一直重试,还没等执行成功,此时如果项目「重启」了,那这次重试请求也就「丢失」了,那这条数据就一直不一致了。因此我们就需要在另一个服务中完成重发操作
  2. 消息队列能保证队列里的消息在成功被消费之前不会丢失
  3. 消息队列可以保证消息被成功传递,否则还会继续投递

img

==订阅Binlog==

当然业务层也可以不发起重发操作,因为更新数据库的时候会将信息写入binlog日志中,通过订阅该日志就能获取到key,之后发布删除操作到MQ中。常用的如阿里的Canal

img

总结

在保证最终一致性的情况下,可以采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做

串行化能达到强一致性的,但会降低缓存的作用。

Redis并发竞争key

如果多个子系统同时设置一个key,可以:

(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下

1
2
3
系统A key1 {valueA  3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}

那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!