Redis整理复习

Redis简介

在没有redis之前,客户端访问后端应用时,当并发大的时候,存储层如mysql是支撑不了的,可能会将存储层mysql压死,存储层一旦宕机,整个应用就完了。为了很高效的加速应用的读写速度,同时也可以降低后端负载在一定程度上可以通过AOF和RDB机制保证在一定的情况下(如缓存层宕机)快速恢复数据为应用提供服务。缓存层可以通过主从复制+哨兵或集群实现高可用。Redis(REmote DIctionary Server)是一个开源的使用ANSI C语言编写的基于内存亦可持久化的日志型,key-value数据库,并提供了多种语言。它通常被称为数据结构服务器,因为值可以是string,hash,list,set,zset. Redis采用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务,Redis执行命令的速度非常快,官方给出的数字是读写性能可以达到10万/秒,在单线程的架构下为什么能如此快呢?主要有四点原因:①redis的所有数据是存放在内存中的②redis是C语言实现的,C语言实现的程序距离操作系统更近③Redis使用了单线程架构,预防了多线程可能产生的竞争问题,例如,CPU频繁切换,资源竞争等问题。④Redis源代码非常少,可以说是精打细磨的。

模块原文连接

Redis的安装

从Redis官网进行下载(https://redis.io/)

安转环境

由于 Redis 是基于 C 语言编写的,因此首先需要安装 Redis 所需要的依赖:

yum install -y gcc tcl gcc-c++ make

上传安装文件

将下载好的 redis-6.2.7.tar.gz 安装包上传到虚拟机的任意目录(一般推荐上传到 /usr/local/src目 录)。

解压安装文件

上传后执行如下命令来进行解压。

tar -zxvf redis-6.2.7.tar.gz

进入安装目录

解压完成后,执行如下命令进入解压目录。

cd redis-6.2.7

运行编译命令

make && make install

说明:如果在编译过程中出现 Jemalloc/jemalloc.h:没有那个文件 没有的错误,在确 保 gcc 安装成功后,可执行 make distclean 进行清除后再次安装。

如果没有出错,就会安装成功。默认的安装路径是在 /usr/local/bin 目录下。可以将这个目 录配置到环境变量中,这样就可以在任意目录下运行这些命令了。主要的几个命令说明如下:

  • redis-server:它是 redis 的服务端启动脚本 redis-cli:它是 redis 提供的客户端启动脚本
  • redis-sentinel:它是 redis 提供的哨兵启动脚本
  • redis-benchmark:性能测试工具,可以在自己电  脑上运行来查看性能
  • redis-check-aof:修复有问题的AOF文件
  • redis-check-dump:修复有问题的dump.rdb文件

启动

在这里我并推荐前台启动,在使用该种方式时,没有办法进行其他的操作,除非另外打开一个session。

后台启动

在 redis 的安装目录中,有一个 redis.conf 文件,我们把这个文件复制到 /etc/目录下

cp /usr/local/redis/redis.conf /etc/

然后修改 /etc/redis.conf 文件,把 daemonize 值设置为 yes 即可

vim /etc/redis.conf # 修改daemonize no

 保存退出后,执行如下命令来启动服务

bin>redis-server /etc/redis.conf

验证服务

我们可以使用 redis-cli 脚本来连接 redis 服务

redis-cli -p 6379

然后执行如下命令

127.0.0.1:6379> ping
PONG

如果能够看到如上信息,表示连接成功。

关闭服务

redis-cli shutdown

关闭后可以执行如下命令来查看进程是否还存在

ps -ef | grep redis

相关知识

redis 默认的端口号是 6379 ,默认有 16 个数据库,类似数组下标从 0 开始,初始默认使用 0 号 数据库。 可以使用 select 命令来切换数据库。例如切换到 2 号数据库是 select 2 。 在 redis 中,可以使用 dbsize 命令来查看当前数据库的 key 的数量,也可以使用 flushdb 命令来清空当前数据库中所有数据,还可以使用 flushall 命令来删除所有数据库中的数据Redis是单线程+多路IO复用技术

多路IO复用

模块原文

为什么 Redis 中要使用 I/O 多路复用这种技术呢?

首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

要弄清问题先要知道问题的出现原因

由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I/O(read,write,accept等等),进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作。阻塞很正常, 接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服
务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端write(sockfd,but,size)发来数据。在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器仍不能处理,仍然阻塞在read(sockfd1,...)上。此时问题就出现了,不能及时处理另一个客户的服务,肿么办?I/O多路复用来解决!

继续上面的问题,有多个客户连接,sockfd1、sockfd2、sockfd3..sockfdn同时监听这n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后就调用read读取收到消息的sockfd,然后又循环回select阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息。

Q:
那这样子,在读取socket1的数据时,如果其它socket有数据来,那么也要等到socket1读取完了才能继续读取其它socket的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程I/O有什么区别呢?

A:
1.CPU本来就是线性的,不论什么都需要顺序处理,并行只能是多核CPU。

2.I/O多路复用本来就是用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题,跟多线程没关系。

3.跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源。而I/O多路复用不需要切换线/进程,效率相对较高,特别是对高并发的应用nginx就是用I/O多路复用,故而性能极佳。但多线程编程逻辑和处理上比I/O多路复用简单,而I/O多路复用处理起来较为复杂。

 什么是多路IO复用技术

I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。

 select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。

多路分离函数select

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

如上图所示,用户线程发起请求的时候,首先会将socket添加到select中,这时阻塞等待select函数返回。当数据到达时,select被激活,select函数返回,此时用户线程才正式发起read请求,读取数据并继续执行。

从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

Reactor(反应器模式)

 如上图,I/O多路复用模型使用了Reactor设计模式(Reactor模式是一个基于事件分发的模式。主要用于IO多路复用。)实现了这一机制。通过Reactor的方式,可以将用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起I/O请求时,数据已经到达了,用户线程一定不会被阻塞。

总结

I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

Redis基本数据类型

类比java

  • string --> String
  • hash --> Hashmap
  • list --> LinkList
  • set --> HashSet
  • sorted_set --> TreeSe
  • Bitmaps
  • HyperLogLog

Redis数据的存储格式

  • redis自身是一个Map类型的存储方式,其中所有的数据都是采用key:value的形式存储
  • 我们讨论的数据类型指的是存储的数据的类型,也就是value部分的类型,key部分永远都是字符串

Jedis的使用

在使用 Jedis 连接 Redis 之前,服务器需要做如下操作

1、关闭防火墙(或放行接口)

systemctl stop firewalld.service
systemctl disable firewalld.service

2、redis开启远程访问

  1. 把 bind 值注释
  2. 把 protected-mode 的值设置为 no

3.创建项目

注解

<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

测试连接

public class testJedis {
    private Jedis jedis;
    @Before
    public void init(){
         jedis = new Jedis("192.168.20.134",6379);
    }

    @After
    public void destroy() {
        jedis.close();
    }

    @Test
    public void test_001(){
        String ping = jedis.ping();
        System.out.println(ping);
    }
}

测试Key

    @Test
    public void testKey() {
        jedis.set("name", "张三");
        jedis.set("age", "18");
        String name = jedis.get("name");
        System.out.println(name);
        Set<String> keys = jedis.keys("*");//返回数据库中所有的key
        keys.forEach(System.out::println);
        System.out.println(jedis.dbSize());//siz()
        // 设置过期时间
        //jedis.expire("name", 20L);
        // 获取过期时间
        System.out.println(jedis.ttl("name"));
        jedis.flushDB();
    }

测试string

    @Test
    public void testString() {
// 设置 key 对应的 value
        jedis.set("course", "redis");
// 根据 key 获取值
        System.out.println(jedis.get("course"));
// 设置过期时间

//jedis.setex("open", 30L, "123");
// 获取过期时间值
        System.out.println(jedis.getEx("open", GetExParams.getExParams()));
// 获取动态的过期时间
        System.out.println(jedis.ttl("open"));
// 同时设置多个值
        jedis.mset("k1","v1","k2","v2","k3","v3");
// 获取多个值
        List<String> values = jedis.mget("k1", "k2");
        values.forEach(System.out::println);
    }

测试list

    @Test
    public void testList() {
// 添加数据
        jedis.lpush("mylist", "a");
        jedis.lpush("mylist", "b");
        jedis.lpush("mylist", "c");
// 获取指定 key 的最左的数据
        String mylist = jedis.lpop("mylist");
        System.out.println(mylist);
// 获取指定 key 的数据个数
        System.out.println(jedis.lpop("mylist", 2));
        System.out.println(jedis.lrange("mylist", 0, -2));
        System.out.println(jedis.llen("mylist"));
    }

测试set

@Test
public void testSet() {
// 添加数据
//jedis.sadd("myredis", "string", "list", "set", "zset", "hash", "bitmaps");
// 获取数据个数
//System.out.println(jedis.scard("myredis"));
// 获取数据
Set<String> myredis = jedis.smembers("myredis");
myredis.forEach(System.out::println);
// 判断指定key和值 是否在集合中
System.out.println(jedis.sismember("myredis", "string1"));
}

测试zset

@Test
public void testZset() {
// 添加数据
jedis.zadd("lisi", 98, "java");
jedis.zadd("lisi", 100, "math");
jedis.zadd("lisi", 99, "english");
// 获取指定分数段的个数
System.out.println(jedis.zcount("lisi", 98, 100));
// 获取指定key和成员对应的分数
System.out.println(jedis.zscore("lisi", "java"));
// 获取指定范围的成员
System.out.println(jedis.zrange("lisi", 0, -1));
// 返回有序集合中指定成员的索引
System.out.println(jedis.zrank("lisi", "java"));
}

测试hash

@Test
public void testHash() {
// 添加数据
jedis.hset("student", "name", "张三");
jedis.hset("student", "age", "20");
jedis.hset("student", "gender", "男");
jedis.hset("student", "birth", String.valueOf(new Date()));
// 获取
System.out.println(jedis.hget("student", "name"));
System.out.println(jedis.hgetAll("student"));
// 批量添加
HashMap<String, String> map = new HashMap<>();
map.put("phone", "123456789");
map.put("email", "abc@openlab.com");
map.put("address", "西安");
jedis.hmset("info", map);
// 批量获取
System.out.println(jedis.hmget("info", "phone", "email", "address"));
}

4.9测试geo

@Test
public void testGeo() {
// 添加城市的经度和纬度值
/*Map<String, GeoCoordinate> memberCoordinateMap = new HashMap<>();
memberCoordinateMap.put("chongqing", new GeoCoordinate(106.50, 29.53));
memberCoordinateMap.put("shanghai", new GeoCoordinate(121.47, 31.23));
memberCoordinateMap.put("shenzhen", new GeoCoordinate(114.05, 22.52));
memberCoordinateMap.put("beijing", new GeoCoordinate(116.38, 39.90));
jedis.geoadd("china:city", memberCoordinateMap);*/
// 获得指定地区的坐标值 geopos key member [member...]
System.out.println(jedis.geopos("china:city", "chongqing"));
// 获取两个位置之间的直线距离 geodist key member1 member2 [m\|km\|ft\|mi]
System.out.println(jedis.geodist("china:city", "chongqing", "beijing",
GeoUnit.KM));
// 以给定的经纬度为中心,找出某一半径内的元素 georadius key longitude latitude
radius [m\|km\|ft\|mi]
List<GeoRadiusResponse> georadius = jedis.georadius("china:city",
106.49999767541885, 29.529999579006592, 5000, GeoUnit.KM);
georadius.forEach(g -> {
System.out.println(g.getDistance());
System.out.println(new String(g.getMember()));
});
}

Springboot整合Redis

添加依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<artifactId>spring-boot-starter-parent</artifactId>
		<groupId>org.springframework.boot</groupId>
		<version>2.7.1</version>
	</parent>
	<groupId>org.example</groupId>
	<artifactId>SpringBootAndJedisTest</artifactId>
	<version>1.0-SNAPSHOT</version>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- redis启动器 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<version>2.7.1</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.11.1</version>
		</dependency>
		<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>

创建配置类

/**
 * Redis配置类
 */
@EnableCaching
@Configuration
public class CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置序列化对象
        RedisSerializer<String> serializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL,
                JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        template.setConnectionFactory(factory);
        // key序列化方式
        template.setKeySerializer(serializer);
        // value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                Jackson2JsonRedisSerializer(Object.class);
// 用于解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config =
                RedisCacheConfiguration.defaultCacheConfig()
                        .entryTtl(Duration.ofSeconds(600))
                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                                jackson2JsonRedisSerializer))
                        .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

启动类@SpringBoot

测试

/**
 * redisTemplate.opsForValue() 获取的是 string类型
 * redisTemplate.opsForHash() 获取hash类型
 * redisTemplate.opsForSet() 获取set类型
 * redisTemplate.opsForList() 获取list类型
 * redisTemplate.opsForZSet() 获取zset类型
 * redisTemplate.opsForHyperLogLog() 获取hyperLogLog类型
 * redisTemplate.opsForGeo() 获取geo类型
 */

@SpringBootTest
public class RedisApiTest {

    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void testRedis() {
        // 添加数据
        redisTemplate.opsForValue().set("hello", "world");
        // 获取数据
        Object hello = redisTemplate.opsForValue().get("hello");
        System.out.println(hello);
    }
}

订阅与发布

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消 息。Redis 客户端可以订阅任意数量的频道

 当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客 户端

命令演示

执行命令订阅一个通道

127.0.0.1:6379> subscribe redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1

在另一个客户端中执行消息发布

127.0.0.1:6379> publish redisChat "hello message"
(integer) 1
127.0.0.1:6379> publish redisChat "hello world"
(integer) 1

发布完成后,在第一个客户端中就可以看到所订阅的频道所传过来的消息。        

127.0.0.1:6379> subscribe redisChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
1) "message"
2) "redisChat"
3) "hello message"
1) "message"
2) "redisChat"
3) "hello world"

事务管理

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

Multi、Exec、Discard

从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入 Exec 后, Redis 会将之前的命令队列中的命令依次执行。 组队的过程中可以通过 discard 来放弃组队。

组队成功,提交成功地情况

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK

组队阶段报错,提交失败的情况:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

组队成功,提交失败的情况:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> incr k5
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

事务冲突问题

问题场景

有一账户余额为 10000 元,现在三个请求,第一个请求想给金额减 8000 元,第二个请求想给金 额减 5000 元,第三个请求想给金额减 1000 元。如果没有事务,可能会发生如下情况。 如果没有事务来控制的话,那么账户就会出现透支的问题。解决这个问题有以下两种方式。

悲观锁

悲观锁(Pessimistic Lock)顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以 每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型 数据库就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

乐观锁(Optimistic Lock)顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所 以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本 号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-andset机制实现事务的。

watch

在执行 multi 之前,先执行 watch key1 [key2] 可以监视一个(或多个) key ,如果在事务执行 之前这个/些 key 被其他命令所改动,那么事务将被打断。

Redis事务的特征

Redis 的事务有以下三个特性:

  1.  单独的隔离操作 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端 发送来的命令请求所打断。
  2.  没有隔离级别的概念 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执 行。
  3. 不保证原子性 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

数据持久化

Redis提供了主要提供了 2 种不同形式的持久化方式: RDB(Redis数据库):

RDB 持久性以指定的时间间隔执行数据集的时间点快照。

AOF(Append Only File):AOF 持久化记录服务器接收到的每个写操作,在服务器启动时再 次播放,重建原始数据集。 命令使用与 Redis 协议本身相同的格式以仅附加方式记录。 当日 志变得太大时,Redis 能够在后台重写日志。

RDB:

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是Snapshot快照,它恢复时是将快照 文件直接读到内存里。

RDB备份

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

Fork

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

在 Linux 程序中,fork() 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统 调用,出于效率考虑,Linux 中引入了“写时复制技术”。

exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务

  • exec系列函数在执行时会直接替换掉当前进程的地址空间

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

 执行流程

 相关配置

快照持久化是Redis中默认开启的持久化方案,根据redis.conf中的配置,快照将被写入dbfilename 指定的文件中(默认是dump.rdb文件)。

注意:RDB功能在 Redis中默认是开启的,而 dump.rdb 文件生成的目录是执行 redis-server 命令所在的目录。

设置文件路径

根据redis.conf中的配置,快照将保存在dir选项指定的路径上,我们可以修改为指定目录

dir ./

修改这个值即可。注意要以绝对路径来表示。

save

格式:save 秒钟 写操作次数

save 3600 1
save 300 100
save 60 10000

RDB的优势

RDB 方式适合大规模的数据恢复,并且对数据完整性和一致性要求不高更适合使用。它有以下几 种优势:

  1. 节省磁盘空间
  2. 恢复速度快

RDB的优势

  1. Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  2. 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  3. 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一 次快照后的所有修改。

AOF

以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不 记录), 只追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据。简单说, Redis 重启时会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 在Redis的默认配置中AOF(Append Only File)持久化机制是没有开启的,要想使用AOF持久化需 要先开启此功能。AOF持久化会将被执行的写命令写到AOF文件末尾,以此来记录数据发生的变 化,因此只要Redis从头到尾执行一次AOF文件所包含的所有写命令,就可以恢复AOF文件的记录 的数据集。

AOF持久化流程

 1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内。

2)AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作sync同步到磁盘的 AOF 文件中。

3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量。

4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。

使用AOF

修改redis.conf文件

  • 通过修改redis.conf配置中 appendonly yes 来开启AOF持久化
  • 通过appendfilename指定日志文件名字(默认为appendonly.aof)
  • 通过appendfsync指定日志记录频率

 当 AOF 和 RDB 同时存在时,以 AOF 持久化文件 appendonly.aof 文件中的内容为准。

恢复

如果 appendonly.aof 文件出问题了,可以使用 redis-check-aof 命令来进行恢复

AOF优势

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

AOF劣势

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

主从复制原理

概述

1、redis的复制功能是支持多个数据库之间的数据同步。一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

2、通过redis的复制功能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。


全同步过程:

  1. Slave 发送 Sync 命令到 Master。
  2. Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中。
  3. Master 将保存数据快照期间接收到的写命令缓存起来。
  4. Master 完成写文件操作后,将该文件发送给 Slave。
  5. 使用新的 RDB 或 AOF 文件替换掉旧的 RDB 或 AOF 文件。
  6. Master 将这期间收集的增量写命令发送给 Slave 端。

增量同步过程如下:

  1. Master 接收到用户的操作指令,判断是否需要传播到 Slave。
  2. 将操作记录追加到 AOF 文件。
  3. 将操作传播到其他 Slave:对齐主从库;往响应缓存写入指令。
  4. 将缓存中的数据发送给 Slave。

————————————————

缓存穿透

描述

用户想要查询某个数据,在 Redis 中查询不到,即没有缓存命中,这时就会直接访问数据库进行 查询。当请求量超出数据库最大承载量时,就会导致数据库崩溃。这种情况一般发生在非正常 URL 访问,目的不是为了获取数据,而是进行恶意攻击。

 现象

  • 一直查询数据库
  • 应用服务器压力变大
  • redis缓存命中率降低

原因

一个不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从 存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失 去了缓存的意义。

解决

① 对空值缓存:如果一个查询数据为空(不管数据是否存在),都对该空结果进行缓存,其过期 时间会设置非常短。

② 设置可以访问名单:使用bitmaps类型定义一个可以访问名单,名单id作为bitmaps的偏移量, 每次访问时与bitmaps中的id进行比较,如果访问id不在bitmaps中,则进行拦截,不允许访问。

③ 采用布隆过滤器:布隆过滤器可以判断元素是否存在集合中,他的优点是空间效率和查询时间 都比一般算法快,缺点是有一定的误识别率和删除困难。

④ 进行实时监控:当发现 Redis 缓存命中率急速下降时,迅速排查访问对象和访问数据,将其设 置为黑名单

缓存击穿

key中对应数据存在,当key中对应的数据在缓存中过期,而此时又有大量请求访问该数据,由于 缓存中过期了,请求会直接访问数据库并回设到缓存中,高并发访问数据库会导致数据库崩溃。

 现象

1、数据库访问压力瞬时增加

2、Redis中没有出现大量 Key 过期

3、Redis正常运行

4、数据库崩溃

原因

由于 Redis 中某个 Key 过期,而正好有大量访问使用这个 Key,此时缓存无法命中,因此就会直 接访问数据层,导致数据库崩溃。 最常见的就是非常“热点”的数据访问。

解决

① 预先设置热门数据:在redis高峰访问时期,提前设置热门数据到缓存中,或适当延长缓存中 key过期时间。

② 实时调整:实时监控哪些数据热门,实时调整key过期时间。

③ 对于热点key设置永不过期。

缓存雪崩

key中对应数据存在,在某一时刻,缓存中大量key过期,而此时大量高并发请求访问,会直接访 问后端数据库,导致数据库崩溃。

注意:缓存击穿是指一个key对应缓存数据过期,缓存雪崩是大部分key对应缓存数据过期

正常情况

 缓存失效瞬间

 现象

1、数据库压力变大导致数据库和 Redis 服务崩溃

原因

在极短时间内,查询大量 key 的集中过期数据

解决

① 构建多级缓存机制:nginx缓存 + redis缓存 + 其他缓存。

② 设置过期标志更新缓存:记录缓存数据是否过期,如果过期会触发另外一个线程去在后台更新 实时key的缓存。

③ 将缓存可以时间分散:如在原有缓存时间基础上增加一个随机值,这个值可以在1-5分钟随 机,这样过期时间重复率就会降低,防止大量key同时过期。

④ 使用锁或队列机制:使用锁或队列保证不会有大量线程一次性对数据库进行读写,从而避免大 量并发请求访问数据库,该方法不适用于高并发情况

哨兵模式

Redis哨兵模式_Double-V的博客-CSDN博客_redis 哨兵模式

概述

(自动选主机的方式)
主从切换技术:当主机宕机后,需要手动把一台从(slave)服务器切换为主服务器,这就需要人工干预,费时费力,还回造成一段时间内服务不可用,所以推荐哨兵架构(Sentinel)来解决这个问题。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
 

 

这里哨兵模式有两个作用:

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

当一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此可以使用哨兵进行监控, 各个哨兵之间还会进行监控,这就形成了多哨兵模式。
 

 以上过程:假设主服务器宕机,哨兵1先检测到结果,但是系统并不会马上进行failover过程,仅仅是哨兵1主观认为主服务器不可以用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover故障转移操作。
操作转移成功后。就会发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这一过程称为  客观下线
优点:
1.哨兵集群。基于主从复制模式,所有的主从配置优点,它都有
2.主从可以切换,故障可以转移,系统可用性就更好
3.哨兵模式是主从模式的升级,手动到自动,更完善
缺点:
1.Redis不好在线扩容,集群容量一旦达到上限,扩容麻烦
2.实现配置麻烦
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值