Redis使用记录

文章目录

1. Windows下安装Redis

1.1 Redis-x64-5.0.14.1安装

(1)下载地址:https://github.com/tporadowski/redis/releases
在这里插入图片描述
(2)下载之后解压,进入文件夹,打开cmd命令,输入服务启动命令:redis-server.exe redis.windows.conf,出现下图则成功启动
在这里插入图片描述
(3)此时redis服务已启动成功。再打开一个cmd窗口,输入启动客户端命令:redis-cli.exe -h 127.0.0.1 -p 6379
设置键值对:set name '张三'
取出键值对:get name

1.2 RESP安装

下载地址:https://download.csdn.net/download/qq_27630263/86890824
(1)下载之后解压即可使用。
双击resp.exe启动RESP。
接下来连接Redis服务。
在这里插入图片描述
打开控制台,输入redis命令。
在这里插入图片描述

2 Redis常用命令

2.1 Redis存储数据的结构

常用的5种数据结构:

  • key-string:一个key对应一个值
  • key-hash:一个key对应一个Map
  • key-list:一个key对应一个列表
  • key-set:一个key对应一个集合
  • key-zset:一个key对应一个有序的集合

另外三种数据结构:

  • HyperLogLog:计算近似值
  • GEO:地理位置
  • BIT:位图,一般存储的也是一个字符串,存储的是一个byte[]

五种常用的存储数据结构图:
在这里插入图片描述
五种常用数据结构的适用场景:

  • key-string:最常用,一般用于存储一个值
  • key-hash:存储一个对象数据
  • key-list:使用list结构实现栈和队列结构
  • key-set:交集,差集和并集的操作
  • key-zset:排行榜,积分存储等操作

2.2 string常用命令

更多命令: http://redisdoc.com/string/index.html#
(1)添加值
set key value

(2)取值
get key

(3)批量操作
mset key value [key value]
mget key [key]

(4)自增命令(自增1)
incr key

(5)自减命令(自减1)
decr key

(6)自增或自减指定数量
incrby key increment
decrby key increment

(7)设置值的同时设置生存时间(每次向redis中添加数据时,尽量都设置生存时间)
setex key second value

(8)设置值,如果当前key不存在的话(如果这个key存在,什么事都不做,如果这个key不存在,和set命令一样)
setnx key value

(9)在key对应的value后,追加内容
append key value

(10)查看value字符串的长度
strlen key

2.3 hash常用命令

(1)存储数据
hset key field value

(2)获取数据
hget key field

(3)批量操作
hmset key field value [field value ...]
hmget key field [field ...]

(4)自增(指定自增的值)
hincrby key field increment

(5)设置值(如果key-field不存在,那么就正常添加,如果存在,什么事都不做)
hsetnx key field value

(6)检查field是否存在
hexists key field

(7)删除key对应的某个或多个field
hdel key field [field...]

(8)获取当前hash结构中的全部field和value
hgetall key

(9)获取当前hash结构中的全部field
hkeys key

(10)获取当前hash结构中的全部value
hvals key

(11)获取当前hash结构中field的数量
hlen key

2.4 list常用命令

(1)存储数据(从左侧插入数据,从右侧插入数据)
lpush key value [value...]
rpush key value [value...]

(2)存储数据(如果key不存在,什么事都不做,如果key存在,但是不是list机构,什么都不做)
lpushx key value
rpushx key value

(3)修改数据(修改指定索引位置的值,如果index超出整个列表的长度会失败)
lset key index value

(4)弹栈方式获取数据(左侧/右侧弹出数据)
lpop key
rpop key

(5)获取指定索引范围的数据(start从0开始,stop输入-1,代表最后一个,-2代表倒数第二部)
lrange key start stop

(6)获取指定索引位置的数据
lindex key index

(7)获取整个列表的长度
llen key

(8)删除列表中的数据(他是删除当前列表中的count个value值,count>0从左侧向右侧删除,count<0从右侧向左侧删除),count=0,删除列表全部的value
lrem key count value

(9)保留列表中的数据(保留指定范围内的数据,超过整个索引范围被移除掉)
ltrim key start stop

(10)将一个列表中最后的一个数据,插入到另外一个列表的头部位置
rpoplpush list1 list2

2.5 set常用命令

(1)存储数据
sadd key member [member...]

(2)获取数据(获取全部数据)
smembers key

(3)随机获取一个数据(获取的同时移除数据,count默认为1,代表弹出数据的数量)
spop key [count]

(4)交集(取多个set集合交集)
sinter set1 set2...

(5)并集(获取全部集合中的数据)
sunion set1 set2...

(6)差集(获取多个集合中不一样的数据,使用第一个集合进行取进行比较)
sdiff set1 set2...

(7)删除数据
srem key member [member...]

(8)查看当前的set集合中是否包含这个值
sismember key member

2.6 zset常用命令

(1)添加数据(score必须是数值,member不允许重复)
zadd key score member [score member...]

(2)修改member的分数、(如果member是存在于key中的,正常增加分数,如果member不存在,这个命令相当于zadd)
zincrby key increment member

(3)查看指定的member的分数
zscore key member

(4)获取zset中数据的数量
zcard key

(5)根据score的范围查询member数量
zcount key min max

(6)删除zset中的成员
zrem key member [member...]

(7)根据分数从小到大排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrange key start stop [withscores]

(8)根据分数从大到小排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrevrange key start stop [withscores]

(9)根据分数的范围获取member(withscores代表同时返回score,添加limit,就和MySQL中一样,如果不希望等于min或者max的值被查询出来,可以采用‘(分数’ 相当于<或者>,但不等于,是一个开区间,最大值和最小值使用+inf和-inf来标识)
zrangebyscore key min max [withsores] [limit offset count]

(10)根据分数的范围获取member(withscores代表同时返回score,添加limit,就和MySQL中一样)
zrevrangebyscore key max min [withsores] [limit offset count]

2.7 key常用命令

(1)查看Redis中的全部key(pattern:,xxx,*xxx)
keys pattern

(2)查看某一个key是否存在(1 - key存在,0 - key不存在)
exists key

(3)删除key
del key [key...]

(4)设置key的生存时间,单位为秒,单位为毫秒.
expire key second
pexpire key milliseconds

(5)设置key的生存时间,单位为秒,单位为毫秒,设置能活到什么时间点
expireat key timestamp
pexpireat key milliseconds

(6)查看key的剩余生存时间,单位为秒,单位为毫秒(-2 - 当前key不存在;-1 - 当前key没有设置生存时间;具体的生存时间)
ttl key
pttl key

(7)移除key的生存时间(1 - 移除成功,0 - key不存在生存时间,key不存在)
persist key

(8)选择操作的库
select 0~15

(9)移动key到另外一个库中
move key db

2.8 库的常用命令

(1)清空当前所在的数据库
flushdb

(2)清空全部数据库
flushall

(3)查看当前数据库中有多少个key
dbsize

(4)查看随后一次操作的时间
lastsave

(5)实时监控Redis服务接收到的命令
monitor

2.9 发布订阅功能

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,该功能被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等
Redis客户端可以订阅任意数量的频道!
在这里插入图片描述

订阅端:

D:\Redis>redis-cli -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> subscribe lzh
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lzh"
3) (integer) 1
1) "message"
2) "lzh"
3) "123"
1) "message"
2) "lzh"
3) "1"

发送端:

D:\Redis>redis-cli -p 6379
127.0.0.1:6379> publish lzh 123
(integer) 2
127.0.0.1:6379> publish lzh 1
(integer) 2
127.0.0.1:6379>

在这里插入图片描述

如要退订某个频道或模式,可使用如下指令:
unsubscribe unsubscribe [channel [channel …]]—取消订阅指定频道。
punsubscribe punsubscribe [pattern [pattern …]]—取消订阅符合指定模式的频道。

3 Java连接Redis

Jedis连接Redis,Lettuce连接Redis

3.1 Jedis连接Redis

jedis所有方法和redis里的所有命令一模一样,没有任何改变,在redis上能操作的,使用jedis同样可以操作

(1)引入依赖

 <!--1、Jedis依赖包-->
 <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
 <dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
 </dependency>
 <!--2、Junit测试-->
 <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13</version>
 </dependency>
 <!--3、Lombok依赖包-->
 <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.18.12</version>
 </dependency>

(2)测试代码

public class Demo1 {
   @Test
   public void set(){
       //1、连接Redis
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2、操作Redis - redis的命令是什么jedis对应的方法就是什么
       jedis.set("name","zhangsan");
       //3、释放资源
       jedis.close();
  }
   @Test
   public void get(){
       //1、连接Redis
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2、操作Redis - redis的命令是什么jedis对应的方法就是什么
       String value = jedis.get("name");
       System.out.println(value);
       //3、释放资源
       jedis.close();
  }
}

3.2 Jedis如何存储一个对象到Redis

(1)准备一个User实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

   private Long id;

   private String name;

   private Date birthday;
}

(2)导入spring-context依赖

<!--4、导入spring-context-->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>4.3.20.RELEASE</version>
</dependency>

(3) 创建Demo测试类,编写内容

public class Demo2 {
   //存储对象 -- 以byte[]形式存储在redis中
   @Test
   public void setByteArray(){
       //1、连接redis服务
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2.1 准备key(String) - value(User)
       String key = "user";
       User user = new User(1L,"张三",new Date());
       //2.2 将key和value转换为byte[]
       byte[] byteKey = SerializationUtils.serialize(key);
       //user对象序列化和反序列化,需要在User类实现Serializable接口
       byte[] byteValue = SerializationUtils.serialize(user);
       //2.3 将key和value存储到redis
       jedis.set(byteKey,byteValue);
       //3、释放资源
       jedis.close();
  }
   @Test
   public void getByteArray(){
       //1、连接redis服务
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2.1 准备key(String)
       String key = "user";
       //2.2 将key转换为byte[]
       byte[] byteKey = SerializationUtils.serialize(key);
       //2.3 获取value
       byte[] byteValue = jedis.get(byteKey);
       //2.4 将value反序列化为user对象
       User user2 = (User)SerializationUtils.deserialize(byteValue);
       System.out.println(user2);
       //3、释放资源
       jedis.close();
  }
}

3.3 Jedis如何存储一个对象到Redis,以String的形式存储

(1)导入一个fastjson依赖

<!--5、导入fastjson-->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.71</version>
</dependency>

(2) 编写测试类

public class Demo3 {
   //存储的对象,以String形式
   @Test
   public void setString(){
       //1、连接redis
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2.1 准备key(String) - value(User)
       String stringKey = "stringUser";
       User value = new User(2L,"李四",new Date());
       //2.2 使用fastjson将value格式化为json字符串
       String stringVlue = JSON.toJSONString(value);
       //2.3 存储到redis中
       jedis.set(stringKey,stringVlue);
       //3关闭连接
       jedis.close();
  }
   @Test
   public void getString(){
       //1、连接redis
       Jedis jedis = new Jedis("127.0.0.1",6379);
       //2.1 准备key
       String stringKey = "stringUser";
       //2.2 去redis中查询value
       String stringValue =jedis.get(stringKey);
       //2.3 将value反序列化为User
       User user = JSON.parseObject(stringValue,User.class);
       System.out.println(user);
       //3关闭连接
       jedis.close();
  }
}

3.4 Jedis连接池的操作

@Test
public void pool2(){
   //1、创建连接池的配置信息
   GenericObjectPoolConfig config = new GenericObjectPoolConfig();
   //连接池中最大的活跃数
   config.setMaxTotal(100);
   //最大空闲数
   config.setMaxIdle(10);
   //最大空闲数
   config.setMinIdle(5);
   //当连接池空了之后,多久没获取到jedis对象就超时,单位毫秒
   config.setMaxWaitMillis(3000);
   //2、创建连接池
   JedisPool pool = new JedisPool(config,"127.0.0.1",6379);
   //3、获取jedis
   Jedis jedis = pool.getResource();
   //4、操作
   String value = jedis.get("stringUser");
   System.out.println(value);
   //6、释放连接
   jedis.close();
}

3.5 Redis的管道操作

因为在操作Redis的时候,执行一个命令需要先发送请求到Redis服务器,这个过程需要经历网络延迟,Redis还需要给客户端一个响应。
如果我需要一次性执行很多个命令,上述的方式效率很低,可以通过Redis的管道,先将命令放到客户端的一个pipeline中,之后一次性的将全部命令发送到Redis服务器,Redis服务一次性的将全部的返回结果响应给客户端。

//Redis的管道操作
@Test
public void pipeline(){
   //1、创建连接
   JedisPool pool = new JedisPool("127.0.0.1",6379);
   long start = System.currentTimeMillis();
   //2、获取一个连接对象
   Jedis jedis = pool.getResource();
//       //3、执行incr - 10000次
//       for (int i = 0; i < 50000; i++) {
//           jedis.incr("pp");
//       }
//       //4、释放资源
//       jedis.close();
   //------------------

   //3、创建管道
   Pipeline pipeline = jedis.pipelined();
   //4、执行incr - 10000次放到管道中
   for (int i = 0; i < 50000; i++) {
       pipeline.incr("qq");
   }
   pipeline.syncAndReturnAll();
   //5、释放资源
   jedis.close();
   long end = System.currentTimeMillis();
   System.out.println(end-start);
}

4 Redis其他配置

4.1 Redis设置密码

(1)修改redis.windows.conf:添加requirepass password
(2)启动Redis:redis-server.exe redis.windows.conf
(3)RESP连接Redis
在这里插入图片描述
(4)Java连接Redis

//第一种(不推荐):
jedis.auth(password);
//第二种:使用JedisPool的方式
public JedisPool(GenericObjectPoolConfig poolConfig, String host, int port, int timeout, String password)

4.2 Redis的事务

Redis 事务的本质是一组命令的集合。一个事务中所有命令会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

4.2.1 相关命令

  • multi:开启事务,redis会将后续的命令逐个放入队列中,然后使用exec 命令来原子化执行这个命令系列。
  • exec:执行事务中的所有操作命令。
  • discard:取消事务,放弃执行事务块中的所有命令。
  • watch:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • unwatch:取消watch对所有key的监视

标准的事务执行:
给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI  #开启事务
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC  #提交事务
1) OK
2) OK
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"22"

事务取消:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 33
QUEUED
127.0.0.1:6379> set k2 34
QUEUED
127.0.0.1:6379> DISCARD  #取消事务
OK

4.2.2 事务出现错误的处理

  • (1)语法错误,会使事务提交失败,数据恢复原样

在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> sets k2 22
(error) ERR unknown command `sets`, with args beginning with: `k2`, `22`, 
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>
  • (2)运行时错误,事务会跳过错误的命令继续执行

在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k1 v2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 11
QUEUED
127.0.0.1:6379> lpush k2 22
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>

4.2.3 乐观锁

  • (1)watch 命令可以为 Redis 事务提供 check-and-set (CAS)行为。

被 watch 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 exec 执行之前被修改了, 那么整个事务都会被取消, exec 返回 nil 来表示事务已经失败。

  • (2)watch是如何监视实现的呢?

Redis使用 watch 命令来决定事务是继续执行还是回滚,那就需要在 multi 之前使用 watch 来监控某些键值对,然后使用 multi 命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用 exec 执行事务时,首先会比对 watch 所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的 watch 命令。
在这里插入图片描述
watch 命令实现监视
在事务开始前用 watch 监控k1,之后修改k1为11,说明事务开始前k1值被改变,multi 开始事务,修改k1值为12,k2为22,执行 exec,发回 nil,说明事务回滚;查看下k1、k2的值都没有被事务中的命令所改变。

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get k1
"11"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>

unwatch 取消监视

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> WATCH k1
OK
127.0.0.1:6379> set k1 11
OK
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 12
QUEUED
127.0.0.1:6379> set k2 22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"12"
127.0.0.1:6379> get k2
"22"
127.0.0.1:6379>
  • (3)事务执行步骤

通过上文命令执行,很显然Redis事务执行是三个阶段:
  1.开启:以 multi 开始一个事务
  2.入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  3.执行:由 exec 命令触发事务

当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
  如果客户端发送的命令为 exec 、 discard、 watch、 multi四个命令的其中一个, 那么服务器立即执行这个命令。
  与此相反, 如果客户端发送的命令是 exec 、 discard、 watch、 multi 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。

4.2.4 深入理解

(1)为什么 Redis 不支持回滚?

如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

以下是这种做法的优点:

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要支持回滚,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。

(2)如何理解Redis与事务的ACID?

一般来说,事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。这是基础,但是很多文章对Redis 是否支持ACID有一些异议,我觉的有必要梳理下:

  • 原子性atomicity

首先通过上文知道,运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。
Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。

  • 一致性consistency

redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

  • 隔离性Isolation

redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。
但是,Redis不像其它结构化数据库有隔离级别这种设计。

  • 持久性Durability

redis事务是不保证持久性的,这是因为redis持久化策略都是异步执行的,不保证持久性是出于对性能的考虑

4.3 Redis持久化机制

Redis是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了两种持久化方式,RDB和AOF。

4.3.1 RDB方式-默认

RDB是把内存数据以快照的形式保存到磁盘上。和AOF相比,它记录的是某一时刻的数据,并不是操作。

什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。

RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。
在这里插入图片描述
RDB通过bgsave命令的执行全量快照,可以避免阻塞主线程。bgsave命令会fork一个子进程,然后该子进程会负责创建RDB文件,而服务器进程会继续处理命令请求。

快照时,数据能修改嘛? Redis接入操作系统的写时复制技术(copy-on-write,COW),在执行快照的同时,正常处理写操作。

虽然bgsave执行不会阻塞主线程,但是频繁执行全量快照也会带来性能开销。比如bgsave子进程需要通过fork操作从主线程创建出来,创建后不会阻塞主线程,但是创建过程是会阻塞主线程的。可以做增量快照

  • RDB的优点:与AOF相比,恢复大数据集的时候会更快,它适合大规模的数据恢复场景,如备份,全量复制等
  • 缺点:没办法做到实时持久化/秒级持久化。

Redis4.0开始支持RDB和AOF的混合持久化,就是内存快照以一定频率执行,两次快照之间,再使用AOF记录这期间的所有命令操作。

RDB触发机制主要有以下几种:

  1. RDB持久化文件,速度比较快,而且存储的是一个二进制的文件,传输起来很方便。
  2. RDB持久化的时机:
    save 900 1 #在900秒内,有1个key改变,就执行RDB持久化
    save 300 10 #在300秒内,有10个key改变,就执行RDB持久化
    save 60 10000 #在60秒内,有10000个key改变,就执行RDB持久化
  3. RDB无法保证数据的绝对安全

#开启RDB持久化的压缩
rdbcompression yes
#RDB持久化文件的名称
dbfilename dump.rdb

4.3.2 AOF方式

AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到AOF文件的末尾。

Redis默认情况是不开启AOF的。重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。

AOF是执行完命令后才记录日志的。为什么不先记录日志再执行命令呢?这是因为Redis在向AOF记录日志时,不会先对这些命令进行语法检查,如果先记录日志再执行命令,日志中可能记录了错误的命令,Redis使用日志回复数据时,可能会出错。

正是因为执行完命令后才记录日志,所以不会阻塞当前的写操作。但是会存在两个风险:

    1. 刚执行完命令还没记录日志时宕机了,会导致数据丢失。
    1. AOF不会阻塞当前命令,但是可能会阻塞下一个操作。

这两个风险最好的解决方案是折中妙用AOF机制的三种写回策略 appendfsync:

    1. always,同步写回,每个子命令执行完,都立即将日志写回磁盘。
    1. everysec,每个命令执行完,只是先把日志写到AOF内存缓冲区,每隔一秒同步到磁盘。
    1. no,只是先把日志写到AOF内存缓冲区,有操作系统去决定何时写入磁盘。

always同步写回,可以基本保证数据不丢失,no策略则性能高但是数据可能会丢失,一般可以考虑折中选择everysec

如果接受的命令越来越多,AOF文件也会越来越大,文件过大还是会带来性能问题。日志文件过大怎么办呢?AOF重写机制!就是随着时间推移,AOF文件会有一些冗余的命令如:无效命令、过期数据的命令等等,AOF重写机制就是把它们合并为一个命令(类似批处理命令),从而达到精简压缩空间的目的。

AOF重写会阻塞嘛?AOF日志是由主线程会写的,而重写则不一样,重写过程是由后台子进程bgrewriteaof完成。

  • AOF的优点:数据的一致性和完整性更高,秒级数据丢失。
  • 缺点:相同的数据集,AOF文件体积大于RDB文件。数据恢复也比较慢。

Redis官方推荐同时开启RDB和AOF持久化,更安全,避免数据丢失。在aof无法使用的时候,再用rdb的备份文件做替补恢复。

  1. AOF持久化的速度相对RDB较慢,存储的是一个文本文件,时间久了文件会比较大,传输困难
  2. AOF持久化机制:
    #每执行一个写操作,立即持久化到AOF文件中,性能比较低
    appendfsync always
    #每秒执行一次持久化
    appendfsync everysec
    #会根据你的操作系统不同,环境的不同,在一定时间执行一次持久化
    appendfsync no
  3. AOF相对RDB更安全,推荐同时开启AOF和RDB。

#AOF主要配置项
#代表开启AOF持久化
appendonly yes
#AOF文件的名称
appendfilename "redis.aof"

同时开启RDB和AOF的注意事项:

  • 如果同时开启了AOF和RDB持久化,那么Redis宕机重启之后,需要加载一个持久化文件,优先选择AOF文件。
  • 如果先开启了RDB,然后之后开启AOF,可能导致RDB先执行了持久化,那么RDB文件中的内容会被AOF覆盖掉。

4.3.3 配置说明

#1.打开RDB持久化配置:
#RDB持久化策略 默认三种方式,[900秒内有1次修改],
#[300秒内有10次修改],[60秒内有10000次修改]即触发RDB持久化,
#我们可以手动修改该参数或新增策略
save 900 1
save 300 10
save 60 10000 
#RDB文件名
dbfilename dump.rdb
#RDB文件存储路径
dir ./
#策略配置:在seconds秒内有changes次数据修改就触发RDB持久化

#2.开启AOF持久化配置
appendonly yes
#AOF文件名
appendfilename "appendonly.aof"
#AOF文件存储路径 与RDB是同一个参数,共用一个文件路径
dir ./  #即bin目录下
#AOF策略:
#[always:每个命令都记录],
#[everysec:每秒记录一次],
#[no:看机器的心情高兴了就记录,linux一般半个小时同步一次]
#appendfsync always
appendfsync everysec
# appendfsync no
#aof文件大小比起上次重写时的大小,增长100%(配置可以大于100%)时,触发重写。
#[假如上次重写后大小为10MB,当AOF文件达到20MB时也会再次触发重写,以此类推
auto-aof-rewrite-percentage 100 
#aof文件大小超过64MB*2时,触发重写,
#为何要乘以2,因为auto-aof-rewrite-percentage 100 是翻倍即100%,
#达到翻倍时才重写
auto-aof-rewrite-min-size 64mb 

#6.打开混合持久化:
#6.aof-use-rdb-preamble yes # 检查混合持久化是否打开,redis5.0后默认开启

4.4 Redis主从架构

单机版Redis存在读写瓶颈的问题
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

  • 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
  • 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有 内存用作Redis存储内存,一般来说,单台Redis大使用内存不应该超过20G。

主从复制: 指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。Redis的主从复制是异步复制,异步分为两个方面,一个是master服务器在将数据同步到slave时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的。
在这里插入图片描述

4.4.1 主从复制的作用

  • 保存Redis数据副本:
    如果我们只是通过RDB或AOF把Redis的内存数据持久化,毕竟只是在本地,并不能保证绝对的安全,而通过将数据同步slave服务器上,可以保留多一个数据备份,更好地保证数据的安全。

  • 读写分离:
    在配置了主从复制之后,如果master服务器的读写压力太大,可以进行读写分离,客户端向master服务器写入数据,在读数据时,则访问slave服务器,从而减轻master服务器的访问压力。
    在这里插入图片描述

  • 高可用性与故障转移:
    服务器的高可用性是指服务器能提供7*24小时不间断的服务,Redis可以通过Sentinel系统管理多个Redis服务器,当master服务器发生故障时,Sentineal系统会根据一定的规则将某台slave服务器升级为master服务器,继续提供服务,实现故障转移,保证Redis服务不间断。

4.4.2 Redis主从复制分为以下三种方式:

    1. 当master服务器与slave服务器正常连接时,master服务器会发送数据命令流给slave服务器,将自身数据的改变复制到slave服务器。
    1. 当因为各种原因master服务器与slave服务器断开后,slave服务器在重新连上master服务器时会尝试重新获取断开后未同步的数据即部分同步,或者称为部分复制。
    1. 如果无法部分同步(比如初次同步),则会请求进行全量同步,这时master服务器会将自己的rdb文件发送给slave服务器进行数据同步,并记录同步期间的其他写入,再发送给slave服务器,以达到完全同步的目的,这种方式称为全量复制。

4.4.3 工作原理

master服务器会记录一个replicationId的伪随机字符串,用于标识当前的数据集版本,还会记录一个当前数据集的偏移量offset,不管master是否有配置slave服务器,replicationId和offset会一直记录并成对存在。

当master与slave正常连接时,slave使用PSYNC命令向master发送自己记录的旧master的replicationId和offset,而master会计算与slave之间的数据偏移量,并将缓冲区中的偏移数据同步到slave,此时master和slave的数据一致。
而如果slave引用的replicationId太旧了,master与slave之间的数据差异太大,则master与slave之间会使用全量复制的进行数据同步。

4.4.4 主从复制中的key过期问题

我们都知道Redis可以通过设置key的过期时间来限制key的生存时间,Redis处理key过期有惰性删除定期删除两种机制,而在配置主从复制后,slave服务器就没有权限处理过期的key;对于在master上过期的key,在slave服务器就可能被读取,所以master会累积过期的key,积累一定的量之后,发送del命令到slave,删除slave上的key。
如果slave服务器升级为master服务器 ,则它将开始独立地计算key过期时间,而不需要通过master服务器的帮助。

4.4.5 Windows下配置主从节点

1、复制Redis安装文件
在这里插入图片描述

2、修改从库文件中 redis.windows.conf 的端口号port 6379
slave1:将默认6379改为6380;
slave2:将默认6379改为6381;

3、安装、启动服务
(1)安装服务:
进入slave文件夹,cmd输入此命令:
redis-server --service-install redis.windows.conf --service-name Redis6380
(2)启动服务:然后去服务中,开启“redis6380”(此时就可以连接6380的库了)
(3)启动从节点:redis-cli -p 6380
(4)先启动主节点服务,然后
   连接主节点,并开启数据同步:slaveof 127.0.0.1 6379
   断开连接,关闭数据同步:slaveof no one
(5)输入密码:auth 123456
(6)即可读取值: get key从库默认是不允许写入数据的。

4、永久保存主从关系
修改从节点配置文件
(1)按如下所示,添加命令 slaveof 127.0.0.1 6379 ,配置好以后,每次redis服务重启时,会自动同步主库数据。
在这里插入图片描述
(2)先启动主节点服务,然后启动从节点服务:redis-server.exe redis.windows.conf
(3)启动从节点客户端:redis-cli -p 6380
(4)输入密码:auth 123456
(5)即可读取值: get key
info可以查看节点信息

4.5 哨兵

哨兵可以帮助我们解决主从架构中的单点故障问题

主从模式存在的问题:在主从复制模式中,我们可以发现其系统不具备自动恢复的功能,所以当主服务器(master)宕机后,需要手动把一台从服务器(slave)切换为主服务器。在这个过程中,不仅需要人工干预,将从节点晋升为主节点,而且还要通知应用方更新主节点地址,造成一段时间内服务器处于不可用状态,同时数据安全性也得不到保障,因此主从模式的可用性较低,不适用于线上生产环境。

解决方案之哨兵模式:Redis 官方推荐一种高可用方案,也就是Redis Sentinel 哨兵模式,它弥补了主从模式的不足。Sentinel 通过监控的方式获取主机的工作状态是否正常,当主机发生故障时, Sentinel 会自动进行Failover(即故障转移),并将其监控的从机提升主服务器(master),从而保证了系统的高可用性。

4.5.1 哨兵模式原理

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

下面使用 Sentinel 搭建 Redis 集群,基本结构图如下所示:
在这里插入图片描述
在上图过程中,哨兵主要有两个重要作用:

  1. 哨兵节点会以每秒一次的频率对每个 Redis 节点发送PING命令,并通过 Redis 节点的回复来判断其运行状态。
  2. 当哨兵监测到主服务器发生故障时,会自动在从节点中选择一台将机器,并其提升为主服务器,然后使用 PubSub 发布订阅模式,通知其他的从节点,修改配置文件,跟随新的主服务器。

4.5.2 多哨兵模式

在实际生产情况中,哨兵是集群的高可用的保障,为避免 一个哨兵节点 发生意外,它一般是由 3~5 个节点组成,并且各个哨兵之间还会互相进行监控,这样就算挂了个别节点,该集群仍然可以正常运转。

其结构图如下所示:
在这里插入图片描述
以上过程:假设主服务器宕机,哨兵1先检测到结果,但是系统并不会马上进行failover过程,仅仅是哨兵1主观认为主服务器不可以用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover故障转移操作。
操作转移成功后。就会发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这一过程称为客观下线

多哨兵模式的工作过程:
(1)主观下线
主观下线,适用于主服务器和从服务器。如果在规定的时间内(配置参数:down-after-milliseconds),Sentinel 节点没有收到目标服务器的有效回复,则判定该服务器为“主观下线”。比如 Sentinel1 向主服务发送了PING命令,在规定时间内没收到主服务器PONG回复,则 Sentinel1 判定主服务器为“主观下线”,这个时候系统并不会马上进行failover过程,因为仅仅是Sentinel1主观的认为主服务器不可用。
(2) 客观下线
客观下线,只适用于主服务器。 Sentinel1 发现主服务器出现了故障,它会通过相应的命令,询问其它 Sentinel 节点对主服务器的状态判断。如果超过半数以上的 Sentinel 节点认为主服务器 down 掉,则 Sentinel1 节点判定主服务为“客观下线”。
(3) 投票选举
投票选举,所有 Sentinel 节点会通过投票机制,按照谁发现谁去处理的原则,选举 Sentinel1 为领头节点去做 Failover(故障转移)操作。Sentinel1 节点则按照一定的规则在所有从节点中选择一个最优的作为主服务器,然后通过发布订阅功能通知其余的从节点(slave)更改配置文件,跟随新上任的主服务器(master)。至此就完成了主从切换的操作。

工作流程小结:

(1)Sentinel 负责监控主从节点的“健康”状态。当主节点挂掉时,自动选择一个最优的从节点切换为主节点。
(2)客户端来连接 Redis 集群时,会首先连接 Sentinel,通过 Sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互
(3)当主节点发生故障时,客户端会重新向 Sentinel 要地址,Sentinel 会将最新的主节点地址告诉客户端。因此应用程序无需重启即可自动完成主从节点切换。

4.5.3 搭建哨兵模式

由于哨兵模式是基于主从模式上的,所以需要先进行搭建主从模式。

若是网络连接则需要先进行如下操作:
1.先在网络部分注释掉单机连接那一行,即注释掉bind 127.0.0.1
2.然后将后台运行打开:daemonize no,设置为yes。
3.再将保护模式关闭:protected-mode yes 改为:protected-mode no

  1. 配置sentinel哨兵
    新建配置文件sentinel.conf,并进行如下配置
port 26379
sentinel monitor myredis 127.0.0.1 6379 1
sentinel auth-pass myredis 123456 

配置文件说明:
port 26379 #sentinel监听端口,默认是26379,可以更改
sentinel monitor <master-name> <ip> <redis-port> <quorum>
<master-name>:服务器的名称,可以自定义。
<ip>:代表监控的主服务器。
<redis-port>:代表主服务器端口。
<quorum>:是一个数字,表示当有多少个 sentinel 认为主服务器宕机时,它才算真正的宕机掉,例如写2,那么表示只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。通常数量为半数或半数以上才会认为主机已经宕机<quorum> 需要根据 sentinel 的数量设置(大于3个哨兵的,建议投票数为:哨兵数/2 -1)。
sentinel auth-pass <master-name> password 配置主节点密码

  1. 启动sentienl哨兵
    执行命令:linux: redis-sentinel sentinel.conf
         windows: redis-server.exe sentinel.conf --sentinel

主机是:6379
从机是:6380,6381

在这里插入图片描述
至此,正常情况下哨兵模式的配置就结束了,下面开始测试。

  1. 停止主服务器服务

我们来模拟主服务意外宕机的情况,首先直接将主服务器的 Redis 服务终止,然后查看从服务器是否被提升为了主服务器。

终止master的redis服务:
127.0.0.1:6379> shutdown

  1. 重新查看主从关系

接着我们去查看哨兵的日志,其发现我们的6379主机宕机了,并且过了一会通过选举将我们的从机6380晋升为主机!

在这里插入图片描述

而且sentinel.conf配置文件也发发生了变化:
在这里插入图片描述
若节点设置密码,从节点连接新的主节点时,会出现验证密码的错误,尚待解决!最好不要设置密码

  1. 主机回归

如果我们原来的主机6379重新回来了,那么其只能归并到新的主机下,当做从机。
恢复6379服务器:redis-server redis79.conf

  1. 开启多个哨兵

如果想开启多个哨兵,只需配置要多个sentinel.conf文件即可,然后将端口号进行更改,其他一致,注意<master-name>服务器名字也要一致,然后都启动即可。如下:

哨兵1:
port 26379
sentinel monitor myredis 127.0.0.1 6379 2
哨兵2:
port 26380
sentinel monitor myredis 127.0.0.1 6379 2
由于设置为2,所以只有两个哨兵都发现主机宕机了才会进行重写选举。
注:如果要停止哨兵模式只需要:Ctrl+C 即可停止。

  1. Sentinel配置说明
# Example sentinel.conf
 
# 哨兵sentinel实例运行的端口 默认26379
port 26379
 
# 哨兵sentinel的工作目录
dir /tmp
 
# 哨兵sentinel监控的redis主节点的 ip port 
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# 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因为replication而不可用。
# 可以通过将这个值设为 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
  1. 可能出现的问题

(1)哨兵选举后,无法真正地切换。
在这里插入图片描述

无法切换,有几种情况:

  • 1-redis保护模式开启了

每一台机子下的:redis.conf配置文件,还有哨兵的redis-sentinel.conf 配置文件修改成:
bind 0.0.0.0
protected-mode no

  • 2-端口没有放开;

各个哨兵,端口要能相互telnet 对应的ip 端口
查看想开的端口是否已开:firewall-cmd --query-port=26379/tcp
添加指定需要开放的端口:firewall-cmd --add-port=26379/tcp --permanent
重载入添加的端口:firewall-cmd --reload
查询指定端口是否开启成功:firewall-cmd --query-port=26379/tcp
返回yes即可

  • 3-master密码和从密码不一致。

由于哨兵配置的时候没有配置从密码,只配置了master的密码,那么问题来了,如果master挂掉了,哨兵sentinel切换master的时候,怎么去修改其他节点的配置信息呢。实际上,哨兵是拿master的密码去认证的,所以,我们在配置redis的时候,建议redis的账号密码一致(至少主账号的master-auth密码和从节点的一致)

  • 4-master节点的redis.conf没有添加masterauth

master节点也要设置masterauth,避免当master重启后无法变成新master节点的从节点
masterauth password

4.5.4 Java代码连接哨兵

public class Demo4 {

   public void sentinelTest(){
       	JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMinIdle(5);
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.157.6",26379).toString());
        sentinels.add(new HostAndPort("192.168.157.6",26380).toString());
        sentinels.add(new HostAndPort("192.168.157.6",26381).toString());
        // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, jedisPoolConfig, 3000, null);
        Jedis jedis = jedisSentinelPool.getResource();
        System.out.println(jedis.set("single", "zhuge"));
        System.out.println(jedis.get("single"));
		// 注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
		jedis.close();
  }
}

4.6 Redis集群

4.6.1 Redis集群介绍

一、为什么需要Redis集群?

在讲Redis集群架构之前,我们先简单讲下Redis单实例的架构,从最开始的一主N从,到读写分离,再到Sentinel哨兵机制,单实例的Redis缓存足以应对大多数的使用场景,也能实现主从故障迁移。
在这里插入图片描述
但是,在某些场景下,单实例存Redis缓存会存在的几个问题
(1)写并发:
Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
(2)海量数据的存储压力:
单实例Redis本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。

针对以上的问题,Redis集群提供了较为完善的方案,解决了存储能力受到单机限制,写操作无法负载均衡的问题

二、什么是Redis集群?

Redis3.0加入了Redis的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。

Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。

Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。
在这里插入图片描述
如上图所示,Redis集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点(其中,只有master节点具有处理请求的能力,slave节点主要是用于节点的高可用)

4.6.2 Redis集群的数据分布算法:哈希槽算法

前面讲到,Redis集群通过分布式存储的方式解决了单节点的海量数据存储的问题,对于分布式存储,需要考虑的重点就是如何将数据进行拆分到不同的Redis服务器上。常见的分区算法有hash算法、一致性hash算法。

  • 普通hash算法:将key使用hash算法计算之后,按照节点数量来取余,即hash(key)%N。优点就是比较简单,但是扩容或者摘除节点时需要重新根据映射关系计算,会导致数据重新迁移。
  • 一致性hash算法:为每一个节点分配一个token,构成一个哈希环;查找时先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。优点是在加入和删除节点时只影响相邻的两个节点,缺点是加减节点会造成部分数据无法命中,所以一般用于缓存,而且用于节点量大的情况下,扩容一般增加一倍节点保障数据负载均衡。

Redis集群采用的算法是哈希槽分区算法。Redis集群中有16384个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分的哈希槽。在对数据进行操作的时候,集群会对使用CRC16算法对key进行计算并对16384取模(slot = CRC16(key)%16383),得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的Redis节点,然后直接到这个对应的节点上进行存取操作。

使用哈希槽的好处就在于可以方便的添加或者移除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;哈希槽数据分区算法具有以下几种特点:

  • 解耦数据和节点之间的关系,简化了扩容和收缩难度;
  • 节点自身维护槽的映射关系,不需要客户端代理服务维护槽分区元数据
  • 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景

默认情况下,redis集群的读和写都是到master上去执行的,不支持slave节点读和写,跟Redis主从复制下读写分离不一样,因为redis集群的核心的理念,主要是使用slave做数据的热备,以及master故障时的主备切换,实现高可用的。Redis的读写分离,是为了横向任意扩展slave节点去支撑更大的读吞吐量。而redis集群架构下,本身master就是可以任意扩展的,如果想要支撑更大的读或写的吞吐量,都可以直接对master进行横向扩展。

4.6.3 Redis集群中节点的通信机制:goosip协议

redis集群的哈希槽算法解决的是数据的存取问题,不同的哈希槽位于不同的节点上,而不同的节点维护着一份它所认为的当前集群的状态,同时,Redis集群是去中心化的架构。那么,当集群的状态发生变化时,比如新节点加入、slot迁移、节点宕机、slave提升为新Master等等,我们希望这些变化尽快被其他节点发现,Redis是如何进行处理的呢?也就是说,Redis不同节点之间是如何进行通信进行维护集群的同步状态呢?

在Redis集群中,不同的节点之间采用gossip协议进行通信,节点之间通讯的目的是为了维护节点之间的元数据信息。这些元数据就是每个节点包含哪些数据,是否出现故障,通过gossip协议,达到最终数据的一致性。

gossip协议,是基于流行病传播方式的节点或者进程之间信息交换的协议。原理就是在不同的节点间不断地通信交换信息,一段时间后,所有的节点就都有了整个集群的完整信息,并且所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,但只要这些节可以通过网络连通,最终他们的状态就会是一致的。Gossip协议最大的好处在于,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。

Redis集群中节点的通信过程如下:

  • 集群中每个节点都会单独开一个TCP通道,用于节点间彼此通信。
  • 每个节点在固定周期内通过待定的规则选择几个节点发送ping消息
  • 接收到ping消息的节点用pong消息作为响应

使用gossip协议的优点在于将元数据的更新分散在不同的节点上面,降低了压力;但是缺点就是元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。另外,由于 gossip 协议对服务器时间的要求较高,时间戳不准确会影响节点判断消息的有效性。而且节点数量增多后的网络开销也会对服务器产生压力,同时结点数太多,意味着达到最终一致性的时间也相对变长,因此官方推荐最大节点数为1000左右。

redis cluster架构下的每个redis都要开放两个端口号,比如一个是6379,另一个就是加1w的端口号16379。

  • 6379端口号就是redis服务器入口。
  • 16379端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用的是一种叫gossip 协议的二进制协议

gossip协议常见的消息类型包含: ping、pong、meet、fail等等。
(1)meet:主要用于通知新节点加入到集群中,通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。

(2)ping:用于交换节点的元数据。每个节点每秒会向集群中其他节点发送 ping 消息,消息中封装了自身节点状态还有其他部分节点的状态数据,也包括自身所管理的槽信息等等。

  • 因为发送ping命令时要携带一些元数据,如果很频繁,可能会加重网络负担。因此,一般每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。
  • 如果发现某个节点通信延时达到了 cluster_node_timeout / 2,那么立即发送 ping,避免数据交换延时过长导致信息严重滞后。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout 可以调节,如果调得比较大,那么会降低 ping 的频率。
  • 每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 (总节点数 - 2)个其它节点的信息。

(3)pong:ping和meet消息的响应,同样包含了自身节点的状态和集群元数据信息。

(4)fail:某个节点判断另一个节点 fail 之后,向集群所有节点广播该节点挂掉的消息,其他节点收到消息后标记已下线。

由于Redis集群的去中心化以及gossip通信机制,Redis集群中的节点只能保证最终一致性。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,也就是Gossip协议是最终一致性的协议。

meet命令的实现:
在这里插入图片描述
(1)节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

(2)节点A根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息。

(3)节点B接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

(4)节点B向节点A返回一条PONG消息。

(5)节点A将收到节点B返回的PONG消息,通过这条PONG消息,节点A可以知道节点B已经成功的接收了自己发送的MEET消息。

(6)之后,节点A将向节点B返回一条PING消息。

(7)节点B将接收到的节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功的接收到了自己返回的PONG消息,握手完成。

(8)之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间后,节点B会被集群中的所有节点认识。

clusterNode数据结构:保存节点的当前状态,比如节点的创建时间,节点的名字,节点当前的配置纪元,节点的IP和地址,等等。
clusterState数据结构:记录当前节点所认为的集群目前所处的状态。

4.6.4 集群的扩容与收缩

作为分布式部署的缓存节点总会遇到缓存扩容和缓存故障的问题。这就会导致缓存节点的上线和下线的问题。由于每个节点中保存着槽数据,因此当缓存节点数出现变动时,这些槽数据会根据对应的虚拟槽算法被迁移到其他的缓存节点上。所以对于redis集群,集群伸缩主要在于槽和数据在节点之间移动。

1、扩容:

(1)启动新节点
(2)使用cluster meet命令将新节点加入到集群
(3)迁移槽和数据:添加新节点后,需要将一些槽和数据从旧节点迁移到新节点

在这里插入图片描述
如上图所示,集群中本来存在“缓存节点1”和“缓存节点2”,此时“缓存节点3”上线了并且加入到集群中。此时根据虚拟槽的算法,“缓存节点1”和“缓存节点2”中对应槽的数据会应该新节点的加入被迁移到“缓存节点3”上面。

新节点加入到集群的时候,作为孤儿节点是没有和其他节点进行通讯的。因此需要在集群中任意节点执行 cluster meet 命令让新节点加入进来。假设新节点是 192.168.1.1 5002,老节点是 192.168.1.1 5003,那么运行以下命令将新节点加入到集群中。
192.168.1.1 5003> cluster meet 192.168.1.1 5002

这个是由老节点发起的,有点老成员欢迎新成员加入的意思。新节点刚刚建立没有建立槽对应的数据,也就是说没有缓存任何数据。如果这个节点是主节点,需要对其进行槽数据的扩容;如果这个节点是从节点,就需要同步主节点上的数据。总之就是要同步数据。
在这里插入图片描述
如上图所示,由客户端发起节点之间的槽数据迁移,数据从源节点往目标节点迁移。

(1)客户端对目标节点发起准备导入槽数据的命令,让目标节点准备好导入槽数据。使用命令:cluster setslot {slot} importing {sourceNodeId}
(2)之后对源节点发起送命令,让源节点准备迁出对应的槽数据。使用命令:cluster setslot {slot} migrating {targetNodeId}
(3)此时源节点准备迁移数据了,在迁移之前把要迁移的数据获取出来。通过命令 cluster getkeysinslot {slot} {count}。Count 表示迁移的 Slot 的个数。
(4)然后在源节点上执行,migrate {targetIP} {targetPort} “” 0 {timeout} keys {keys} 命令,把获取的键通过流水线批量迁移到目标节点。
(5)重复 3 和 4 两步不断将数据迁移到目标节点。
(6)完成数据迁移到目标节点以后,通过 cluster setslot {slot} node {targetNodeId} 命令通知对应的槽被分配到目标节点,并且广播这个信息给全网的其他主节点,更新自身的槽节点对应表。

2、收缩:

  • 迁移槽。
  • 忘记节点。通过命令 cluster forget {downNodeId} 通知其他的节点

在这里插入图片描述

为了安全删除节点,Redis集群只能下线没有负责槽的节点。因此如果要下线有负责槽的master节点,则需要先将它负责的槽迁移到其他节点。迁移的过程也与上线操作类似,不同的是下线的时候需要通知全网的其他节点忘记自己,此时通过命令 cluster forget {downNodeId} 通知其他的节点。

4.6.5 集群的故障检测与故障转恢复机制

1、集群的故障检测:

Redis集群的故障检测是基于gossip协议的,集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。

(1)主观下线(pfail):当节点A检测到与节点B的通讯时间超过了cluster-node-timeout 的时候,就会更新本地节点状态,把节点B更新为主观下线。

主观下线并不能代表某个节点真的下线了,有可能是节点A与节点B之间的网络断开了,但是其他的节点依旧可以和节点B进行通讯。

(2)客观下线:

由于集群内的节点会不断地与其他节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点,因此集群内的节点会不断收到下线报告。

当半数以上的主节点标记了节点B是主观下线时,便会触发客观下线的流程(该流程只针对主节点,如果是从节点就会忽略)。将主观下线的报告保存到本地的 ClusterNode 的结构fail_reports链表中,并且对主观下线报告的时效性进行检查,如果超过 cluster-node-timeout*2 的时间,就忽略这个报告,否则就记录报告内容,将其标记为客观下线。

接着向集群广播一条主节点B的Fail 消息,所有收到消息的节点都会标记节点B为客观下线。

2、集群的故障恢复:

当故障节点下线后,如果是持有槽的主节点则需要在其从节点中找出一个替换它,从而保证高可用。此时下线主节点的所有从节点都担负着恢复义务,这些从节点会定时监测主节点是否进入客观下线状态,如果是,则触发故障恢复流程。故障恢复也就是选举一个节点充当新的master,选举的过程是基于Raft协议选举方式来实现的。

2.1、从节点过滤:

检查每个slave节点与master节点断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master。

2.2、投票选举:

(1)节点排序:

对通过过滤条件的所有从节点进行排序,按照priority、offset、run id排序,排序越靠前的节点,越优先进行选举。

  • priority的值越低,优先级越高
  • offset越大,表示从master节点复制的数据越多,选举时间越靠前,优先进行选举
  • 如果offset相同,run id越小,优先级越高

(2)更新配置纪元:

每个主节点会去更新配置纪元(clusterNode.configEpoch),这个值是不断增加的整数。这个值记录了每个节点的版本和整个集群的版本。每当发生重要事情的时候(例如:出现新节点,从节点精选)都会增加全局的配置纪元并且赋给相关的主节点,用来记录这个事件。更新这个值目的是,保证所有主节点对这件“大事”保持一致,大家都统一成一个配置纪元,表示大家都知道这个“大事”了。

(3)发起选举:

更新完配置纪元以后,从节点会向集群发起广播选举的消息(CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST),要求所有收到这条消息,并且具有投票权的主节点进行投票。每个从节点在一个纪元中只能发起一次选举。

(4)选举投票:

如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。

如果超过(N/2 + 1)数量的master节点都投票给了某个从节点,那么选举通过,这个从节点可以切换成master,如果在 cluster-node-timeout*2 的时间内从节点没有获得足够数量的票数,本次选举作废,更新配置纪元,并进行第二轮选举,直到选出新的主节点为止。

在第(1)步排序领先的从节点通常会获得更多的票,因为它触发选举的时间更早一些,获得票的机会更大

2.3、替换主节点:

当满足投票条件的从节点被选出来以后,会触发替换主节点的操作。删除原主节点负责的槽数据,把这些槽数据添加到自己节点上,并且广播让其他的节点都知道这件事情,新的主节点诞生了。

(1)被选中的从节点执行SLAVEOF NO ONE命令,使其成为新的主节点
(2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
(3)新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点
(4)新的主节点开始接收和处理槽相关的请求

如果集群中某个节点的master和slave节点都宕机了,那么集群就会进入fail状态,因为集群的slot映射不完整。
如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。

4.6.6 Redis集群的搭建

Redis集群的搭建可以分为以下几个部分:
1、启动节点:将节点以集群模式启动,读取或者生成集群配置文件,此时节点是独立的。
2、节点握手:节点通过gossip协议通信,将独立的节点连成网络,主要使用meet命令。
3、槽指派:将16384个槽位分配给主节点,以达到分片保存数据库键值对的效果。

Redis集群至少需要三个master节点,并且推荐节点数为奇数。这是因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。 奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。

1. 构建集群节点目录

  • (1)创建一个redis-cluster目录用于存放集群节点
  • (2)拷贝开始下载的redis解压后的目录,并修改文件名(比如按集群下redis端口命名)如下:在这里插入图片描述
  • (3)在每个集群节点目录下创建文件start.bat(注意不同的端口号),可以直接执行此脚本启动redis
title redis-6380;
redis-server.exe redis.windows.conf
  • (4)修改每个集群节点的配置文件(注意端口号)
#修改为与当前文件夹名字一样的端口号
port 6380 
#指定是否在每次更新操作后进行日志记录,Redis在 默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导4. 致一段时间内的数据丢失。 yes表示:存储方式,aof,将写操作记录保存到日志中
appendonly yes 
#设置redis集群密码
masterauth 123456
#设置redis密码
requirepass 123456
#开启集群模式
cluster-enabled yes 
#保存节点配置,自动创建,自动更新(建议命名时加上端口号)
cluster-config-file nodes-6380.conf  
#集群超时时间,节点超过这个时间没反应就断定是宕机
cluster-node-timeout 15000 

上述步骤完成后可以依次点击对应的start.bat文件启动redis

注意:配置项前不能有空格和#,特别注意cluster-enabled yes配置项
注意:启动完成后不能关闭cmd窗口,否之redis就被关闭了,若要关闭参考后面的将redis注册为服务章节

2. 下载Ruby并安装

  • 链接: Ruby下载地址
    Ruby的安装可以参考此教程:链接: Rubby安装教程
    在这里插入图片描述
  • 安装完成需要配置Ruby,打开cmd任意目录输入
    gem install redis
    在这里插入图片描述

3. 构建集群脚本redis-trib.rb
注意:此处很多教程让直接下载redis-trib.rb,然后直接使用,如果redis-trib.rb版本和你的redis版本不对,会报错WARNING: redis-trib.rb is not longer available!
You should use redis-cli instead.

在这里插入图片描述
所以 redis-trib.rb的版本需要和redis一致

查看redis版本:redis-server -v
下载redis-trib.rb
redis-trib.rb存放位置如下:
在这里插入图片描述
4. 构建集群
cmd进入redis集群节点目录后,执行一下命令,中途会询问是否打印更多详细信息,输入yes即可,然后redis-trib 就会将这份配置应用到集群当中,让各个节点开始互相通讯
ruby redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
在这里插入图片描述
在这里插入图片描述
到此集群构建完毕!!!!

4.6.7 Java连接Redis集群

使用JedisCluster对象连接Redis集群

public class Demo5 {

   public void clusterTest(){
       //创建Set<HostAndPort>
       Set<HostAndPort> nodes = new HashSet<>();
       nodes.add(new HostAndPort("192.168.102.11",7001));
       nodes.add(new HostAndPort("192.168.102.11",7002));
       nodes.add(new HostAndPort("192.168.102.11",7003));
       nodes.add(new HostAndPort("192.168.102.11",7004));
       nodes.add(new HostAndPort("192.168.102.11",7005));
       nodes.add(new HostAndPort("192.168.102.11",7006));
       //创建jedisCluster集群对象
       JedisCluster jedisCluster = new JedisCluster(nodes);

       String value = jedisCluster.get("a");
       System.out.println(value);
  }
}

4.6.8 Redis集群的运维

1、数据迁移问题
Redis集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。而 Redis为了保证迁移的一致性,迁移所有操作都是同步操作,执行迁移时,两端的 Redis均会进入时长不等的阻塞状态,对于小Key,该时间可以忽略不计,但如果一旦Key的内存使用过大,严重的时候会接触发集群内的故障转移,造成不必要的切换。

2、带宽消耗问题
Redis集群是无中心节点的集群架构,依靠Gossip协议协同自动化修复集群的状态,但goosip有消息延时和消息冗余的问题,在集群节点数量过多的时候,goosip协议通信会消耗大量的带宽,主要体现在以下几个方面:

  • 消息发送频率:跟cluster-node-timeout密切相关,当节点发现与其他节点的最后通信时间超过 cluster-node-timeout/2时会直接发送ping消息
  • 消息数据量:每个消息主要的数据占用包含:slots槽数组(2kb)和整个集群1/10的状态数据
  • 节点部署的机器规模:机器的带宽上限是固定的,因此相同规模的集群分布的机器越多,每台机器划分的节点越均匀,则整个集群内整体的可用带宽越高

集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗,因此搭建Redis集群需要根据业务数据规模和消息通信成本做出合理规划:

  • 在满足业务需求的情况下尽量避免大集群,同一个系统可以针对不同业务场景拆分使用若干个集群。
  • 适度提供cluster-node-timeout降低消息发送频率,但是cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者平衡
  • 如果条件允许尽量均匀部署在更多机器上,避免集中部署。如果有60个节点的集群部署在3台机器上每台20个节点,这是机器的带宽消耗将非常严重

3、Pub/Sub广播问题
集群模式下内部对所有publish命令都会向所有节点进行广播,加重带宽负担,所以集群应该避免频繁使用Pub/sub功能

4、集群倾斜
集群倾斜是指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。因此需要理解集群倾斜的原因

(1)数据倾斜:

  • 节点和槽分配不均
  • 不同槽对应键数量差异过大
  • 集合对象包含大量元素
  • 内存相关配置不一致

(2)请求倾斜:
合理设计键,热点大集合对象做拆分或者使用hmget代替hgetall避免整体读取

5、集群读写分离
集群模式下读写分离成本比较高,直接扩展主节点数量来提高集群性能是更好的选择。

5 Redis常见问题

5.1 key的生存时间到了,Redis会立即删除吗?

不会立即删除

  1. 定期删除:
    Redis每隔一段时间就会去查看Redis设置了过期时间的key,会在大概100ms的间隔中默认查看3个key.
  2. 惰性删除
    当去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间是否已经到了,直接删除当前key,并且给用户返回一个空值。

5.2 Redis的淘汰机制

在Redis内存已经满的时候,添加一个新的数据,就会执行淘汰策略。

  1. volatile-lru:在内存不足时,Redis会在设置了过期时间的key中淘汰掉一个最近最少使用的key
  2. allkeys-lru:在内存不足时,Redis会在全部的key中淘汰掉一个最近最少使用的key
  3. volatile-lfu:在内存不足时,Redis会在设置了过期时间的key中淘汰掉一个最近最少频次使用的key
  4. allkeys-lfu:在内存不足时,Redis会在全部的key中淘汰掉一个最近最少频次使用的key
  5. volatile-random:在内存不足时,Redis会在设置了过期时间的key中随机淘汰掉一个key
  6. allkeys-random:在内存不足时,Redis会在全部的key中随机淘汰掉一个key
  7. volatile-ttl:在内存不足时,Redis会在设置了过期时间的key中随机淘汰掉一个剩余生存时间最少的key
  8. noeviction:(默认):在内存不足时,直接报错

指定淘汰机制的方式:maxmemory-policy noeviction(具体策略)
设置Redis最大内存:maxmemory

5.3 缓存的常见问题

(1)缓存穿透

问题出现的原因:查询的数据,Redis中没有,数据库中也没有。如何解决?

  1. 根据Id查询时,如果id是自增的,将id的最大值放到Redis中,在查询数据库之前,直接比较一下id.
  2. 如果id不是整形的,可以将全部id放到set中,在用户查询之前,去set中查看一些是否有这个id.
  3. 获取客户端的ip地址,可以将ip的访问添加限制。
  4. 将访问的key直接在Redis中缓存一个空值,下次访问的时候可直接查redis放回空值
  5. 根据缓存数据Key的设计规则,将不符合规则的key采用布隆过滤器进行过滤

(2)缓存击穿

问题出现的原因:缓存中的热点数据,突然到期了,造成大量的请求都去访问数据库,造成数据库宕机

  1. 在访问缓存中没有的时候,添加一个锁,让几个请求去访问数据库,避免数据库宕机
  2. 去掉热点数据的生存时间

(3)缓存雪崩

问题出现的原因:当大量缓存同时到期时,最终大量的同时去访问数据库,导致数据库宕机

  1. 将缓存中的数据设置不同的生存时间,例如设置为30~60分钟的要给随机时间

(4)缓存倾斜

问题出现的原因:热点数据放在一个Reids节点上,导致Redis节点无法承受住大量的请求,最终导致Redis宕机。

  1. 扩展主从架构,搭建多个从节点,缓解Redis的压力
  2. 可以在Tomcat中做JVM缓存,在查询Redis之前,先去查询Tomcat中的缓存。

6 项目中遇到的问题

6.1 常见的16种应用场景

缓存、数据共享分布式、分布式锁、全局 ID、计数器、限流、位统计、购物车、用户消息时间线 timeline、消息队列、抽奖、点赞、签到、打卡、商品标签、商品筛选、用户关注、推荐模型、排行榜.

1、缓存

String类型

例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访问数据。

2、数据共享分布式

String 类型,因为 Redis 是分布式的独立服务,可以在多个应用之间共享

例如:分布式Session

<dependency> 
 <groupId>org.springframework.session</groupId> 
 <artifactId>spring-session-data-redis</artifactId> 
</dependency>

3、分布式锁

String 类型setnx方法,只有不存在时才能添加成功,返回true

public static boolean getLock(String key) {
    Long flag = jedis.setnx(key, "1");
    if (flag == 1) {
        jedis.expire(key, 10);
    }
    return flag == 1;
}
 
public static void releaseLock(String key) {
    jedis.del(key);
}

4、全局ID

int类型,incrby,利用原子性

incrby userid 1000

分库分表的场景,一次性拿一段

5、计数器

int类型,incr方法

例如:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库

6、限流

int类型,incr方法

以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false

7、位统计

String类型的bitcount

字符是以8位二进制存储的

set k1 a
setbit k1 6 1
setbit k1 7 0
get k1 
/* 6 7 代表的a的二进制位的修改
a 对应的ASCII码是97,转换为二进制数据是01100001
b 对应的ASCII码是98,转换为二进制数据是01100010
因为bit非常节省空间(1 MB=8388608 bit),可以用来做大数据量的统计。
*/

例如:在线用户统计,留存用户统计

setbit onlineusers 01 
setbit onlineusers 11 
setbit onlineusers 20

支持按位与、按位或等等操作

BITOPANDdestkeykey[key...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。       
BITOPORdestkeykey[key...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。 
BITOPXORdestkeykey[key...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。 
BITOPNOTdestkeykey ,对给定 key 求逻辑非,并将结果保存到 destkey 。

计算出7天都在线的用户

BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ...  "day_7_online_users"

8、购物车

String 或hash。所有String可以做的hash都可以做

在这里插入图片描述

  • key:用户id;field:商品id;value:商品数量。
  • +1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。

9、用户消息时间线timeline

list,双向链表,直接作为timeline就好了。插入有序

10、消息队列

List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间

  • blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
  • brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

上面的操作。其实就是java的阻塞队列。学习的东西越多。学习成本越低

  • 队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列
  • 栈:先进后出:rpush brpop

11、抽奖

set:自带一个随机获得值

spop myset

12、点赞、签到、打卡
在这里插入图片描述
set:假如上面的微博ID是t1001,用户ID是u3001

用 like:t1001 来维护 t1001 这条微博的所有点赞用户

  • 点赞了这条微博:sadd like:t1001 u3001
  • 取消点赞:srem like:t1001 u3001
  • 是否点赞:sismember like:t1001 u3001
  • 点赞的所有用户:smembers like:t1001
  • 点赞数:scard like:t1001

是不是比数据库简单多了。

13、商品标签

在这里插入图片描述
老规矩,用 tags:i5001 来维护商品所有的标签。

  • sadd tags:i5001 画面清晰细腻
  • sadd tags:i5001 真彩清晰显示屏
  • sadd tags:i5001 流程至极

14、商品筛选

// 获取差集
sdiff set1 set2
// 获取交集(intersection )
sinter set1 set2
// 获取并集
sunion set1 set2

在这里插入图片描述
假如:iPhone11 上市了

sadd brand:apple iPhone11
 
sadd brand:ios iPhone11
 
sad screensize:6.0-6.24 iPhone11
 
sad screentype:lcd iPhone 11

赛选商品,苹果的、ios的、屏幕在6.0-6.24之间的,屏幕材质是LCD屏幕

sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

15、用户关注、推荐模型

follow 关注 fans 粉丝

相互关注:

  • sadd 1:follow 2
  • sadd 2:fans 1
  • sadd 1:fans 2
  • sadd 2:follow 1

我关注的人也关注了他(取交集):

  • sinter 1:follow 2:fans

可能认识的人:

  • 用户1可能认识的人(差集):sdiff 2:follow 1:follow
  • 用户2可能认识的人:sdiff 1:follow 2:follow

16、排行榜

id 为6001 的新闻点击数加1:

  • zincrby hotNews:20190926 1 n6001

获取今天点击最多的15条:​​​​​​​

  • zrevrange hotNews:20190926 0 15 withscores

6.2 Springboot 接入Redis后发现隔一段时间连接会超时 command timed out,错误信息如下:

org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed
 out after 5 second(s)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:273)
    at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.convertLettuceAccessException(LettuceStringCommands.java:799)
    at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)
    at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:266)
    at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57)
    at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:228)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:188)
    at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96)
    at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53)

这是因为springboot2.x之后,默认使用的client是lettuce,而不是jedis了。lettuce的连接池会自动断开,找了很多解决方案都没用,最后还是使用jedis作为redis的client,解决了问题,只需要在pom.xml文件中做如下配置即可

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<!-- 过滤lettuce,使用jedis作为redis客户端 -->
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

面试问题

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值