0、Redis

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库

Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis的优势:

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

官网:https://redis.io/

中文官网:http://www.redis.cn/

1、Linux下的Redis的安装

1.1、下载

下载地址:https://redis.io/download

或者

1
wget http://download.redis.io/releases/redis-5.0.3.tar.gz

1.2、上传文件到服务器

如果是直接使用wget下载的就不用再上传

1.3、解压文件

1
tar -zxvf redis-5.0.3.tar.gz

解压后目录

1.4、安装依赖

redis 是基于C++开发的,所有要安装C++的环境

1
yum -y install gcc-c++ autoconf automake

1.5、预编译

1
2
cd redis-5.0.3 # 进入redis目录
make # 预编译

1.6、安装

创建一个安装目录,不然会安装到一个默认路径,后期路径不好找

1
mkdir -p /usr/local/redis
  • -p:级联创建,上级目录不存在也一同创建

安装到指定路径

1
make PREFIX=/usr/local/redis/ install
  • PREFIX:指定要安装的路径

  • Redis-cli:客户端
  • Redis-server:服务器端

1.7、前台启动

默认就是前台启动,但是前台启动有一个不好的地方,就是Redis会一致占用前台输入,无法进行别的从操作。这是为了方便就需要把Redis设置为后台启动,让它在后台运行,不耽误前台进行别的操作。

1.8、后台启动

1、ctrl+c,结束前台程序运行

2、复制解压的redis目录下的redis.conf文件到安装的路径

1
cp /root/redis-5.0.3/redis.conf /usr/local/redis/bin

3、修改bin目录下的redis.conf文件

1
vim redis.conf

修改daemonize ondaemonize yes

4、再次运行查看

再次启动程序,需要戴胜redis.conf文件,否则还是前台运行

1
2
./redis-server redis.conf	# 启动程序
ps -ef |grep redis # 查看和redis相关的进程

5、后台进程关闭

前台启动的话只需要ctrl+c就可以关闭,但是后台启动相比就麻烦了点,需要直接关闭进程

1
kill -9 5977

5977:进程号

2、windows客户端方法Redis

2.1、安装客户端软件

下载地址:https://lanzous.com/ickc0yd

常规安装,选择安装路径,下一步就可以了

2.2、配置服务器端Redis程序

默认情况下,Redis是不允许别的客户端连接访问的,只能是本机(127.0.0.1)访问,需要设置一下,否则windows客户端连接不上redis

1
vim redis.conf

1、注释掉bind 127.0.0.1

1
# bind 127.0.0.1

如果注释掉bind就表示可以是任何ip访问,如果是想要指定的IP访问,直接在127.0.0.1 后添加指定IP就可以

2、关闭保护模式,设置protected-mode为no,否则依然无法连接

1
protected-mode no

3、设置访问认证(密码)

默认状态下是没有开启认证的,需要手动取消#注释(该命令大概在500行左右)

1
requirepass root

4、保存退出,结束进程,重新启动redis

1
./redis-server redis.conf

2.3、windows客户端连接redis

3、Redis-cli连接操作Redis

Redis-cli就是Redis自带的一个客户端程序,可以用来操作Redis,在安装的路径里

3.1、redis-clid连接redis

1
./redis-cli -p 6379 -a root
  • 6379:redis的默认端口
  • root:认证密码

3.2、操作redis

3.2.1、String类型

1
2
3
4
set name zhangsan	# 添加一个名为name的String类型数据,值为zhangsan
get name # 获取名为name的String类型数据
mset age 18 addr henan # 批量添加String类型数据
mget age addr # 批量获取String类型数据,获取名为age和addr的数据

3.2.2、Hash类型

1
2
3
4
5
6
hset userInfo name list # 添加一个名为UserInfo的hash类型,其中一个名是name,值为list
hget userInfo name # 获取名为name的数据
hmset userInfo age 18 addr henan # 添加多条数据
hmget userInfo age addr # 获取多个数据
hgetall userInfo # 获取全部数据
hdel userInfo addr # 删除指定数据

3.2.3、List类型

这里的List类型比较特殊,分为左右两种,一种是从左边添加,一种是从右边添加。左边添加就类似于一种压栈效果,先进的排到后面,右边添加就是常规的追加。

1
2
lpush students zhangsan lisi # 左边添加
lrange students 0 2 # 遍历list,0起始索引,2结束索引

可以看到zhangsan排在了后面,而lisi排在前面,类似于一个压栈的效果

1
2
rpush students wangwu zhaoliu # 右边添加
lrange students 0 8 # 遍历,结束索引可以是一个比list长度大的数字

可以看到,右边添加wangwu在zhaoliu前边,就是一个追加的效果

1
2
llen students #查看长度
lrem students 1 lisi # 删除指定数量的指定元素
  • lrem students 1 lisi:删除students中的1个lisi

1
2
lpop students # 移除左边开始的第一个元素
rpop students # 移除右边开始的第一个元素

3.2.4、Set类型

这里的set类型和java的一样,无序,不重复

1
2
3
4
sadd letters aaa bbb ccc ddd # 添加set类型数据
smembers letters # 查看set类型数据
scard letters # 获取set类型数据条数
srem letters aaa ccc # 删除set类型里的指定元素

3.2.5、Sorted Set类型

一个有序的Set类型

1
2
3
4
zadd score 5 zhangsan 6 lisi 3 wangwu 9 zhaoliuclear # 添加参数,数值代表等级,数值越低,排名越靠前
zrange score 0 8 # 遍历数据
zcard score # 数据元素条数
zrem score lisi # 删除指定元素

3.2.6、层级关系|目录结构存储数据

1
2
mset user:01 ahh # 以层级关系存储数据,:分割层级
mget user:01 # 以层级关系查找数据,:分割层级

3.2.7、设置Key的失效时间

有几种不同的命令可以设置失效时间

方式一:

1
set code test ex 10 # 10秒后失效

方式二:

1
2
set code test # 添加数据
expire code 10 # 设置10秒后失效
  • expire:秒
  • pexpire:毫秒
  • expireat:秒的时间戳
  • pexpireat:毫秒的时间戳

方式三:

1
set code test nx ex 10 # code存在时才设置失效时间
  • code:key
  • test:value
  • nx:不存在时设置时间,还有一个可选值xx,存在时设置失效时间
  • ex:秒[px:毫秒]

3.2.8、删除

1
2
set code test # 添加数据
del code # 删除数据

4、Redis持久化储存

Redis是内存中的数据结构储存系统,不是直接存储在磁盘上的,所有就有可能会在机器宕机的时候无法有效的储存数据,所以就需要持久化储存。

Redis有三种方式可以用于持久化储存数据

4.1、bgsave

使用bgsave手动持久化储存数据

1
2
set test test
bgsave

这种方式后进行持久化操作很方便,但是很麻烦,需要重复多个的执行bgsave命令

4.2、save配置

Redis还自带了一种save的持久化储存方式,这是一种自动化的储存方式(默认开启)。可以在redis.conf文件中看到(大概220行左右),这种方式会在指定时间内,进行自动保存

默认配置如图,表示在

900秒内,有一个key被改动,则在900秒后被自动持久化保存

300秒内,有10个key被改动,则在300秒后被自动持久化保存

60秒内,有10000个key被改动,则在60秒后被自动持久化保存

但是这种情况也不是很完善,比如在60秒内有10000的数据被改动,但是在58秒的时候机器断电了,不到低60秒,它也不会自动持久化储存。

4.3、appendonly

默认情况就该配置是关闭的,需要手动开启,设置on为yes(redis.conf文件的大概700行左右),该配置开启后会自动关闭上面的save持久化方式。该方式的特点就是会保存写过的所有的命令,会把所有的命令保存到一个appendonly.aof文件中,每次启动redis都会先执行该文件,以达到持久化储存的效果。

但是这种方式也有弊端,那就是会保存很多没用的代码,后续文件会越来越大,影响redis的运行效果。

5、Java操作Redis

5.1、创建SpringBoot项目

5.2、修改POM配置文件

1、修改Test依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

2、修改Redis依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--
1.x的版本默认采用的连接池技术是Jedis
2.x以上版本默认连接时是Lettuce,
如果要使用Jedis连接池需要排除Lettuce的依赖
-->
<!--排除lettuce依赖-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

3、添加Jedis依赖

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

完整文件

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
66
67
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.yanghuisen</groupId>
<artifactId>redisdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redisdemo</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--Redis组件依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--
1.x的版本默认采用的连接池技术是Jedis
2.x以上版本默认连接时是Lettuce,
如果要使用Jedis连接池需要排除Lettuce的依赖
-->
<!--排除lettuce依赖-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Jedis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

5.3、Jedis的的配置文件

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
redis:
# Redis服务器地址
host: 192.168.10.100
# Redis服务器端口
port: 6379
# Redis服务器认证密码
password: root
# 要连接的数据库,默认就是0
database: 0
# 连接超时时间
timeout: 10000ms
jedis:
pool:
# 最大连接数,默认是8
max-active: 1024
# 最大连接组设时间,单位毫秒,默认-1ms
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小连接,默认0
min-idle: 5

5.4、创建连接池配置类

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
package cn.yanghuisen.redisdemo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
* @author admin
* @version 1.0
* @date 2020/5/13 19:22
* @Description Jedis配置类
*/
@Configuration // 配置类
public class JedisConfig {
// 服务器地址
@Value("${spring.redis.host}")
private String host;

// 服务器端口
@Value("${spring.redis.port}")
private Integer port;

// 认证密码
@Value("${spring.redis.password}")
private String password;

// 连接超时时间
@Value("${spring.redis.timeout}")
private String timeout;

// 最大链接数
@Value("${spring.redis.jedis.pool.max-active}")
private Integer maxActive;

// 最大链接数
@Value("${spring.redis.jedis.pool.max-wait}")
private String maxWait;

// 最大空闲连接
@Value("${spring.redis.jedis.pool.max-idle}")
private Integer maxIdle;

// 最小空闲连接
@Value("${spring.redis.jedis.pool.min-idle}")
private Integer minIdle;

@Bean
public JedisPool redisPoolFactory(){
// 连接池配置类
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 设置最大连接阻塞时间
jedisPoolConfig.setMaxWaitMillis(Long.parseLong(maxWait.substring(0,maxWait.length()-2)));
// 最大链接数
jedisPoolConfig.setMaxTotal(maxActive);
// 最大空闲连接
jedisPoolConfig.setMaxIdle(maxIdle);
// 最小空闲连接
jedisPoolConfig.setMinIdle(minIdle);
// 连接池,连接池配置类,服务器地址,端口,连接超时时间。认证密码
return new JedisPool(jedisPoolConfig,host,port,Integer.parseInt(timeout.substring(0,timeout.length()-2)),password);
}
}

5.5、注入连接池,切入点拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Resource
private JedisPool jedisPool;
private Jedis jedis = null;

@BeforeEach
public void initConn(){
jedis = jedisPool.getResource();
}

@AfterEach
public void closeCinn(){
if (null!=jedis){
jedis.close();
}
}

5.5、操作String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 操作String
*/
@Test
void testString(){
// 添加单条数据
jedis.set("ahh","ahh");
jedis.set("age","18");

// 添加多条数据,参数:奇数-key,偶数-value
jedis.mset("addr","henan","sex","男");

// 获取一条数据
System.out.println(jedis.get("ahh"));

// 获取多条数据
jedis.mget("age","addr","sex").forEach(System.out::println);

// 删除
jedis.del("sex");
}

5.6、操作Hash

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
/**
* 操作Hash
*/
@Test
public void testHash(){
// 添加一条数据
jedis.hset("userInfo","name","ahh");
// 添加多条数据
Map<String,String> map = new HashMap<>();
map.put("age","20");
map.put("sex","男");
jedis.hmset("userInfo",map);

// 获取一条数据
System.out.println(jedis.hget("userInfo","name"));
// 获取多条数据
jedis.hmget("userInfo","age","sex").forEach(System.out::println);
// 获取Hash类型多有的数据
jedis.hgetAll("userInfo").forEach((k, v) -> {
System.out.println(k+"----"+v);
});

// 删除
jedis.hdel("userInfo","sex");
}

5.7、操作List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 操作List
*/
@Test
void testList(){
// 左添加
jedis.lpush("aabc","a","b","c");
// 右添加
jedis.rpush("aabc","1","2","3");
// 获取数据
jedis.lrange("aabc",0,10).forEach(System.out::println);
// 获取总条数
System.out.println("总条数:"+jedis.llen("aabc"));
// 删除list元素
jedis.lrem("aabc",1,"c");
// 左移除
jedis.lpop("aabc");
// 右移除
jedis.rpop("aabc");
// 删除
jedis.del("aabc");
}

5.8、操作Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 操作Set
*/
@Test
void testSet(){
// 添加数据
jedis.sadd("letters","aa","bb","cc","dd");
// 获取数据
jedis.smembers("letters").forEach(System.out::println);
// 获取条数
System.out.println("总条数:"+jedis.scard("letters"));
// 删除
jedis.srem("letters","dd");
}

5.9、操作Sorted Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 操作SortedSet
*/
@Test
void testSortedSet(){
// 添加数据
Map<String,Double> map = new HashMap<>();
map.put("张三",5d);
map.put("李四",2d);
map.put("王五",8d);
map.put("赵六",6d);
jedis.zadd("students",map);

// 获取数据
jedis.zrange("students",0,8).forEach(System.out::println);
// 总条数
System.out.println("总条数:"+jedis.zcard("students"));
// 删除
jedis.zrem("students","赵六");
}

5.10、层级关系|目录形式存储数据

1
2
3
4
5
6
7
8
9
10
/**
* 层级关系|目录形式存储数据
*/
@Test
public void testDir(){
jedis.set("users:user:ahh","ahh");
jedis.set("users:user:zhangsan","zhangsan");
System.out.println(jedis.get("users:user:ahh"));
System.out.println(jedis.get("users:user:zhangsan"));
}

5.11、设置失效时间

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
/**
* 设置失效时间
*/
@Test
public void testExpire(){
// 方法1
jedis.set("code","test");
jedis.expire("code",10); // 10秒
jedis.pexpire("code",10000L); // 10000毫秒
System.out.println(jedis.ttl("code")); // 获取剩余秒数据
// 方法2
jedis.setex("code",10,"test"); // 10秒
jedis.psetex("code",10000L,"test"); // 10000毫秒
System.out.println(jedis.pttl("code")); // 获取剩余毫秒
// 方法3
SetParams setParams = new SetParams();
// 不存在时
// setParams.nx();
// 存在时
setParams.xx();
// 设置失效时间-秒
// setParams.ex(10);
// 设置失效时间-毫秒
setParams.px(10000L);
jedis.set("code","test",setParams);
}

5.12、获取所有的key

1
2
3
4
5
6
7
8
9
10
/**
* 获取所有的key
*/
@Test
void testKeyAll(){
// 获取key的数量
System.out.println(jedis.dbSize());
// 获取所有的key
jedis.keys("*").forEach(System.out::println);
}

5.13、删除

1
2
3
4
5
6
7
/**
* 删除
*/
@Test
void testDel(){
jedis.del("userInfo");
}

5.14、事务

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 事务
*/
@Test
void testMulti(){
Transaction tx = jedis.multi();
// 开启事务
tx.set("tel","1010");
// 提交事务
tx.exec();
// 事务回滚
// tx.discard();
}

5.15、byte序列化

序列化和反序列化工具

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
package cn.yanghuisen.redisdemo.utils;

import java.io.*;

/**
* @author admin
* @version 1.0
* @date 2020/5/13 20:40
* @Description 序列化和反序列化工具
*/
public class SerializeUtil {
// 序列化
public static byte[] serialize(Object obj){
// 对象输出流
ObjectOutputStream oos = null;
// 字节数组输出流
ByteArrayOutputStream baos = null;
try {
// 序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

// 反序列化
public static Object unserialize(byte[] bytes){
if (null==bytes){
return null;
}

ByteArrayInputStream bais = null;
try {
// 反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

用户类

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
package cn.yanghuisen.redisdemo.pojo;

import java.io.Serializable;

/**
* @author admin
* @version 1.0
* @date 2020/5/13 20:51
* @Description TODO
*/
public class User implements Serializable {
private Integer id;
private String name;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

数据储存和获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* byte序列化
*/
@Test
void testByte(){
User user = new User();
user.setId(1);
user.setName("ahh");
// 序列化
byte[] userkey = SerializeUtil.serialize("user:"+user.getId());
byte[] uservalue = SerializeUtil.serialize(user);
jedis.set(userkey,uservalue);

// 获取数据
byte[] values = jedis.get(userkey);

// 反序列化
System.out.println(SerializeUtil.unserialize(values));

}

6、Redis搭建主从复用

单机版的Redis可能会出现一个问题就是,一旦服务器宕机就无法使用,所以Redis有一个主从复用的概念,就是说有一个主服务器和从服务器,主服务器提供写入和读取,从服务器提供取的功能,一旦其中一个从服务器出现了问题,还有其它的从服务区提供使用,一旦主服务器出现了问题,可以把其中的一个从服务器变为主服务器。

6.1、读写分离

把Redis分为主从关系,一个主服务器,和两个从服务器。

主服务器提供写入和获取,从服务器只提供获取功能。

1、创建三个目录

  • data:数据文件
  • log:日志文件
  • conf:配置文件

2、复制redis.conf配置文件到创建的配置文件目录下

3、配置配置文件,作为公共配置文件

注释掉bind,运行外界连接

关闭保护模式,否则外界无法连接

注释公共配置端口,后期需要运行三个redis,每个的端口不一样,需要针对每个redis进行私有的配置

修改为后台启动

注释进程编号,三个redis的进程编号都是不一样的,需要针对的进行私有配置

注释公共配置日志文件,三个redis有三个日志文件,私有配置

注释公共配置数据文件,修改数据文件路径为创建的数据文件路径

添加从数据库访问主服务器认证密码

设置访问认证,三个redis的认证密码都是一个,设置为公用的

注释公共配置追加文件,这个根据需求选择是否使用,这里关闭

设置从服务器只能读取,不能写入

保存退出

6.2、创建配置三个私有配置文件

1、创建三个配置文件

redis-xxxx:xxxx表示的每个的端口,为了方便区分

2、修改每个的配置文件

redis-6379.conf

1
2
3
4
5
6
7
8
9
10
11
12
# 引用公共配置文件
include /opt/redis/conf/redis-common.conf
# 进程编号记录文件
pidfile /var/run/redis-6379.pid
# 进程端口号
port 6379
# 日志记录文件
logfile "/opt/redis/log/redis-6379.log"
# 数据记录文件
dbfilename dump-6379.rdb
# 追加文件名称
appendfilename "appendonly-6379.aof"

redis-6380.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 引用公共配置文件
include /opt/redis/conf/redis-common.conf
# 进程编号记录文件
pidfile /var/run/redis-6380.pid
# 进程端口号
port 6380
# 日志记录文件
logfile "/opt/redis/log/redis-6380.log"
# 数据记录文件
dbfilename dump-6380.rdb
# 追加文件名称
appendfilename "appendonly-6380.aof"
# 设置主服务器的IP地址
slaveof 192.168.10.100 6379

redis-6381.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 引用公共配置文件
include /opt/redis/conf/redis-common.conf
# 进程编号记录文件
pidfile /var/run/redis-6381.pid
# 进程端口号
port 6381
# 日志记录文件
logfile "/opt/redis/log/redis-6381.log"
# 数据记录文件
dbfilename dump-6381.rdb
# 追加文件名称
appendfilename "appendonly-6381.aof"
# 设置主服务器的IP地址
slaveof 192.168.10.100 6379

6.3、运行三个redis进程,并测试主从

1、运行三个redis进程

2、查看每个服务器的主从状态

主服务器

从服务器1

从服务器2

3、测试主服务下添加数据,在从服务器下查询是否正常

4、从服务器不能写入测试

6.4、主备切换

上面已经进行了读写分离,完成了主从服务器,但是这时还有一个问题,如果主服务器出现了问题,那么就不能再进行写入操作,只能使用从服务器获取主服务宕机之前的数据,为了解决这一问题,就引入了一个叫做哨兵的方案,通过哨兵,可以检测到主服务是否宕机,如果主服务器宕机了,就会在满足一定的要求后选举一个从服务器变为主服务,如果之前宕机的主服务恢复了,则被宕机的服务变为从服务。以此来解决,主服务宕机后,无法在提供写入操作的问题。

6.4.1、复制哨兵配置文件到创建的配置文件目录下

1
cp /root/redis-5.0.3/sentinel.conf /opt/redis/conf/

6.4.2、修改哨兵配置文件

注释哨兵监听进程端口号

指示哨兵监听一个主服务器,主服务器地址为192.168.10.100,端口为6379,2表示判断失败的要求,配置三个哨兵,一半(3个的一半为1.5,取整为2)以上都监听主服务状态为不达标就更换主服务器

设置密码

设置哨兵认为服务器断线所需的毫秒数。

默认是30000毫秒,也就是30秒,为了测试改为10000毫秒

设置主服务和从服务器切换时间

默认是3分钟,如果三分钟内没有切换成功,则本次切换失败

关闭哨兵的保护模式

修改哨兵为后台启动

6.4.3、添加三个私有哨兵配置文件

私有哨兵配置1

1
2
3
4
5
6
7
8
# 引用公共配置
include /opt/redis/conf/sentinel.conf
# 进程端口号
port 26379
# 进程编号近路文件
pidfile /var/run/sentinel-26379.pid
# 日志文件
logfile "/opt/redis/log/sentinel-26379.log"

私有哨兵配置2

1
2
3
4
5
6
7
8
# 引用公共配置
include /opt/redis/conf/sentinel.conf
# 进程端口号
port 26380
# 进程编号近路文件
pidfile /var/run/sentinel-26380.pid
# 日志文件
logfile "/opt/redis/log/sentinel-26380.log"

私有哨兵配置2

1
2
3
4
5
6
7
8
# 引用公共配置
include /opt/redis/conf/sentinel.conf
# 进程端口号
port 26381
# 进程编号近路文件
pidfile /var/run/sentinel-26381.pid
# 日志文件
logfile "/opt/redis/log/sentinel-26381.log

6.4.4、哨兵启动测试

1、启动三个哨兵

2、查看主服务器和从服务状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RG5Gs2Gg-1589464617789)(http://images.yanghuisen.cn/FitC_MDwSArK5ra8zhbOFGQStflB)]

3、杀死主服务器,模拟主服务宕机

杀死主服务器后等待10秒,让哨兵选择新的主服务器,并重新启动被杀死的服务器

4、查看哨兵选重新选举后的服务器状态

7、SpringDataRedis

SpringBoot1.X版本默认使用的是jedis作为Redsi的Java客户端,而2.X版本使用的是lettuce作为Redis的Java客户端。

两者的区别为:

  • Jedis:在实现上直接连接的Redis-Server,在多个线程间共享一个Jedis实例。线程不安全。如果要在多线程的场景下使用Jedis,需要使用连接池,每个线程都使用自己的Jedis实例,当连接数量增多时,会小号大量的物理资源。
  • lettuce:基于Netty的连接,是一个可伸缩的线程安全的Redis客户端,支持同步、一步和响应式模式。多个线程可以共享一个连接实例,而不必安全多线程并发的问题。它基于NettyNIO框架构建,支持redis的高级功能,如Sentinel(哨兵)、集群、流水线、自动连接和Redis数据模型。

7.1、创建SpringBoot项目

7.2、添加依赖

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
<dependencies>
<!--Spring Data Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--Commons-pool2对象池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!--SpringWeb-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--SpringBootTest-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

7.3、添加application.yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
redis:
# Redis服务器地址
host: 192.168.10.100
# Redis服务器端口
port: 6379
# Redis服务器认证密码
password: root
# Redis数据库
database: 0
# 连接超时时间
timeout: 10000ms
lettuce:
pool:
# 最大连接数,默认8
max-active: 1024
# 最大阻塞等待时间,单位毫秒,默认-1ms
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小空闲连接,默认0
min-idle: 5

7.4、测试环境是否搭建成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
class SpringDataRedisDemoApplicationTests {

@Resource
private RedisTemplate redisTemplate; // 默认模板
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
void initConn() {
ValueOperations<String,String> ops = stringRedisTemplate.opsForValue();
ops.set("userName","ahh");
ValueOperations<String,String> value = redisTemplate.opsForValue();
value.set("name","zhangsan");
System.out.println(ops.get("name"));
}

}

7.5、序列化问题

上面的测试程序,虽然成功的把数据写入了,但是写入的是一个二进制字节码,需要处理一下。

RedisTemplate使用的是JdkSerializationRedisSerializer进行序列化,会序列化为二进制字节码储存。这时需要自定义模板解决。当自定义模板后,又想储存String字符串可以使用StringRedisTemplate。自定义的模板和StringRedisTemplate不冲突。

序列化的选择:

  • JdkSerializationRedisSerializer:该序列化为RedisTemplate的默认序列化工具,是JDK提供的,有点是反序列化时不需要提供类型信息(class),但是缺点就是序列化后的结果非常大,而且看不懂,时json格式的5倍左右,会消耗redis服务器的大量内存。
  • Jackson2JsonRedisSerializer:使用的时Jackson库将对象序列化为json字符串,优点是速度快,序列化后的字符串短小精悍,但是缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息,通过查看源代码,发现其只再反序列化过程中用到了类型信息。
  • GenericJackson2JsonRedisSerializer:通用型序列化,这种序列化方式不用自己手动指定对象的Class。

自定义模板

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
package cn.yanghuisen.springdataredisdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
* @author admin
* @version 1.0
* @date 2020/5/14 20:52
* @Description 自定义模板
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
// 创建模板
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
// 设置String类型的Key的序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置String类型的value的序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置Hash类型的Key的序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置hash类型的value的序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置连接方式
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
return redisTemplate;
}
}

序列化测试

1
2
3
4
5
6
7
8
9
@Test
void testSerial(){
User user = new User();
user.setId(1);
user.setName("ahh");
ValueOperations ops = redisTemplate.opsForValue();
ops.set("user",user);
System.out.println(ops.get("user"));
}

Ok

7.6、操作String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testString(){
ValueOperations vos = redisTemplate.opsForValue();

// 添加一条数据
vos.set("userName","zhangsan");
// 以层级关系|目录形式存储数据
vos.set("user:01","ahh");
// 添加多条数据
Map<String,String> userMap = new HashMap<>();
userMap.put("age","18");
userMap.put("sex","男");
vos.multiSet(userMap);
// 获取一条数据
System.out.println(vos.get("userName"));
// 获取多条数据
List<String> keys = Arrays.asList("userName","age","sex");
vos.multiGet(keys).forEach(System.out::println);
}

7.7、操作Hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
void testHash(){
HashOperations<String,String,String> hos = redisTemplate.opsForHash();
// 添加一条数据
hos.put("userInfo","name","zhangsan");
// 添加多条数据
Map<String,String> map = new HashMap<>();
map.put("age","20");
map.put("sex","男");
hos.putAll("userInfo",map);
// 获取一条数据
System.out.println(hos.get("userInfo","name"));
// 获取多条数据
List<String> keys = Arrays.asList("age","sex");
hos.multiGet("userInfo",keys).forEach(System.out::println);
// 获取Hash类型的所有的数据
hos.entries("userInfo").forEach((k,v)->{
System.out.println(k+"---"+v);
});
// 删除数据
hos.delete("userInfo","name");
}

7.8、操作List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
void testList(){
ListOperations los = redisTemplate.opsForList();
// 左添加
los.leftPush("students","张三");
los.leftPush("students","李四");
// 指定位置前面左添加
los.leftPush("students","李四","王五");
// 右添加
los.rightPush("students","赵六");
// 获取
los.range("students",1,10).forEach(System.out::println);
// 根据下标获取
System.out.println(los.index("students",1));
// 获取总条数
System.out.println(los.size("students"));
// 删除单条数据
los.remove("students",1,"张三");
}

7.9、操作oSet

1
2
3
4
5
6
7
8
@Test
void testSer(){
SetOperations sos = redisTemplate.opsForSet();
// 添加数据
sos.add("letters",new String[]{"aaa","bbb","ccc","ddd"});
sos.members("letters").forEach(System.out::println);
sos.remove("letters","aaa","bbb");
}

7.10、操作SortedSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void testSoredSet(){
ZSetOperations zsos = redisTemplate.opsForZSet();
// 添加数据
zsos.add("score","zhangsan",5D);
zsos.add("score","lisi",2D);
zsos.add("score","wangwu",9D);
zsos.add("score","zhaoliu",3D);
// 获取
zsos.range("score",0,5).forEach(System.out::println);
// 获取总条数
System.out.println(zsos.size("score"));
// 删除
zsos.remove("score","wangwu","zhangsan");
}

7.11、获取所有的Key

1
2
3
4
@Test
void testAllKeys(){
redisTemplate.keys("*").forEach(System.out::println);
}

7.12、删除

1
2
3
4
5
@Test
void testDelete(){
// 删除
redisTemplate.delete("score");
}

7.13、设置Key的失效时间

1
2
3
4
5
6
7
8
9
10
@Test
void testEx(){
ValueOperations vos = redisTemplate.opsForValue();
// 添加一条数据,并设置失效时间
vos.set("code","abcd",180, TimeUnit.SECONDS);
// 给已经存在的key设置失效时间
redisTemplate.expire("code",180,TimeUnit.SECONDS);
// 获取指定key的失效时间
System.out.println(redisTemplate.getExpire("code"));
}

7.14、整合使用哨兵机制

配置文件

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
spring:
redis:
# Redis服务器地址
host: 192.168.10.100
# Redis服务器端口
port: 6380
# Redis服务器认证密码
password: root
# Redis数据库
database: 0
# 连接超时时间
timeout: 10000ms
lettuce:
pool:
# 最大连接数,默认8
max-active: 1024
# 最大阻塞等待时间,单位毫秒,默认-1ms
max-wait: 10000ms
# 最大空闲连接,默认8
max-idle: 200
# 最小空闲连接,默认0
min-idle: 5
# 哨兵模式
sentinel:
# 主节点
master: mymaster
nodes: 192.168.10.100:26379,192.168.10.100:26380,192.168.10.100:26381

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
RedisSentinelConfiguration redisSentinelConfiguration(){
RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration().
// 主节点名称
master("mymaster")
// 服务器地址
.sentinel("192.168.10.100",26379)
.sentinel("192.168.10.100",26380)
.sentinel("192.168.10.100",26381);
// 设置密码
sentinelConfiguration.setPassword("root");
return sentinelConfiguration;
}