Redis的前世今生

Redis的前世今生

基本介绍

数据存储演变过程

image-20200408080531729
  1. 数据存储在文件中:查找数据造成全量扫描,受限于磁盘IO的瓶颈
  2. 关系型数据库:关系型数据库是行级存储,会空出来没有数据列,受限于磁盘IO的瓶颈
  3. 数据库放入缓存:受限于硬件,成本高image-20200408115004299

数据的存储方式受限于:

  1. 冯诺依曼体系的硬件制约
  2. 以太网, TCP/IP 的网络

Redis的特点,对比Memcache , value有类型 , 有类型对应的方法(API) , 计算向数据移动

image-20200408130647248

安装

image-20200408130854366
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
centos 6.x
redis官网5.x
http://download.redis. io/releases/redis-5.0.5.tar.gz
1 , yum install wget
2,cd ~
3,mkdir soft
4,cd soft
5,wget http://download.redis.io/releases/redis-5.0.5.tar.gz
6,tar xf redis.. tar.gz
7,cd redis-src
8,看README md
9, make
.. install gcc
..... make distclean
10,make
11,cdsrc .. .生成了可执行程序
12, cd ..
13,make install PREFIX=/opt/mashibing/redis5
14,vi /etc/profile
export REDIS_ _HOME= /opt/mashibing/redis5
export PATH= $PATH:$REDIS_ _HOME/bin
.source /etc/profile
15,cd utils
16,./install_ server.sh ( 可以执行- -次或多次)
)一个物理机中可以有多个redis实例(进程) ,通过port区分
b)可执行程序就-份在目录,但是内存中未来的多个实例需要各自的配置文件,持久化目录等资源 c) service redis_ 6379 start/stop/stauts > linux /etc/init.d/***
d)脚本还会帮你启动!
17.ps -fe| grep redis
image-20200408153530228 image-20200408184618523 image-20200408184643012

BIO->同步非阻塞NIO->多路复用NIO

内核不断变化

  1. BIO阻塞: 读一个socket产生的文件描述符, 如果数据包没到, read命令就不能返回, 在这阻塞着, 抛出一个线程在这阻塞着, 有数据就处理, 下边的代码执行不了, 其他线程无法处理已到达的数据, socket是阻塞的
    一个线程的成本: 线程栈是独立的, 默认1MB, 线程多了, 调度成本提高. CPU浪费, 占用内存多
  2. 同步非阻塞NIO: 遍历, 取出来处理, 都由自己来完成, 同步非阻塞, 每个连接都要掉一次内核
  3. 多路复用NIO: 内核select(), 允许一个程序监视多个文件描述符, 等待直到一个或多个文件描述符准备好, 就能触发I/O操作了 , 一次系统调用读若干个, 返回有数据的, 减少用户态内核态切换 , 选择有数据的, 直接读
  4. 共享空间: 文件描述符都是累赘, 减少内核区域和用户空间之间传参, 把用户空间和内核空间建立映射, 相当于创建共享空间, 通过mmap系统调用, 红黑树+链表, 进程里有文件描述符就往红黑树里放, 内核可以看到, 把到达的放到链表里, 如果
image-20200408155018832

Redis进程的文件描述符
0: 标准输入 1: 标准输出 2: 报错输出 3,4: pipe调用 5: epoll

kafka: sendfile + mmap
零拷贝: sendfile系统调用

image-20200408202046656

Redis为什么快: epoll : epoll是 Linux内核 为处理大批量 文件描述符 而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量 并发连接 中只有少量活跃的情况下的系统 CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

顺序性: 每个连接内的命令顺序
内存寻址是ns级, 网卡是ms级, 10万倍差距, 10万连接同时时到达, 可能会产生秒级响应
mysql开启缓存, 想模仿redis, 性能反而会低, 多了一次判断过程, 增加了内存空间占用

类比Nginx

image-20200408204003689 image-20200408204441726

5种数据类型

image-20200409061635757

image-20200409075037824

可以根据用户的指令, 看是不是和key里存的type匹配, 不匹配直接返回, 规避异常

image-20200409001118398

nx: 只能新建 分布式锁
xx: 只能更新

String

image-20200409113532196

image-20200409113353283

image-20200409003109666

二进制安全: Redis只取字节流, 一个字符一个字节

image-20200409003158840 image-20200409003251515 image-20200409003647771

和Xshell设置有关

image-20200409003835955

GETSET减少一次I/O

image-20200409004151835

MSETNX原子性set, k2已经存在, 集体失败

image-20200409004430586
bitmap (活跃度|登录数)
image-20200409055641576 image-20200409055518451

按位与

image-20200409060700318 image-20200409060858971

bitmap优势, 举例: 统计用户登录, 按位与, 统计365天, key是天
image-20200409070846180

image-20200409065325283

举例: 共计活跃用户数
第一天a登录, 第二天ab都登陆, 按位或统计活跃用户数, key是日期, value是用户登录情况

image-20200409071145058

image-20200409070205536

list (栈|队列)

image-20200409124546810

同向操作: 栈
反向操作: 队列
正反索引

image-20200409134226364

image-20200409134757688image-20200409134906136

image-20200409134757688 ![image-20200409134906136](Redis的前世今生/image-20200409134906136.png)

hash (点赞|收藏|详情页)

image-20200409192807821

image-20200409184326045 image-20200409184434046 image-20200409184326045 image-20200409184434046 image-20200409184326045 image-20200409184434046 image-20200409184434046 image-20200409184326045 image-20200409184434046 image-20200409184559748

set (交并差集)image-20200409204042872

取交集

image-20200409185350295

取并集

image-20200409185449005

取外差

image-20200409185516331

取随机, 抽奖, 正数不可重复出现, 负数可重复出现
image-20200409190426588

image-20200409190555586

spop随机抽

sorted set (排行榜)

image-20200409203930436 image-20200409194007602 image-20200409204523505 image-20200409203723767 image-20200409205031951 image-20200409205031951 image-20200409205154261
跳表 (随机造层)
image-20200409210122129

图错了

image-20200409210100570 image-20200409210819329

进阶使用

管道Pipeline

客户端连接服务端:

image-20200410053723804

redis 管道 pipeline 一次多条指令

image-20200410053427849
订阅发布

订阅发布, 订阅之后才能收到发布的消息

image-20200410054646720 image-20200410054710442

客户端读历史消息和实时消息
redis实时消息, sorted list 日期排序,

聊天室 : Redis+DB

​ 接收消息:

  • 实时的消息: 通过发布订阅
  • 3天内: sorted_set, 时间作为分值, 消息作为元素
  • 历史记录: DB

​ 发送消息:

  • 一份直接发到Redis的发布订阅, 一份通过Kafka写到数据库
image-20200410055249924

第二种方式实现聊天室: 双实例

image-20200410055947169

Redis事务(无回滚)

  • muti: 开启事务, 所有指令按客户端排队
  • exec: 执行事务(哪个客户端的exec先来, 先执行谁的所有指令)
  • watch: 开启事务之前, 监控某个元素, 发现被更改, 后续相关指令不执行
  • DISCARD: 放弃执行事务, 清空事务队列

image-20200410060037405

演示事务

客户端1: 后开启事务, 删除k1
image-20200410061357607

客户端2: 先开启事务, get不到k1
image-20200612124145896

演示watch: k1改了, 事务不执行

客户端1: 由于k1的值被更改, 相关命令不执行
image-20200410061613062

客户端2:
image-20200410061636588

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

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

布隆过滤器

启动Redis时, 添加布隆过滤器的扩展库

通过bitmap二进制位数组+映射函数

image-20200410063050476

三种方式, 最好放在服务端

image-20200410063202681

image-20200411100955789
安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1,访问redis.io
2,modules
3,访问RedisBloom的github
https://github.com/RedisBloom/RedisBloom
4,linux中wget *.zip
5,yum install unzip
6,unzip *.zip
7,make
8,cp bloom.so /opt/mashibing/redis5/
9,redis-server --loadmodule /opt/mashibing/redis5/redisbloom.so

10 ,redis-cli
11,bf.add ooxx abc
bf.exits abc
bf.exits sdfsdf

12,cf.add # 布谷鸟过滤器

Redis作为缓存和数据库的区别

image-20200411103336509

image-20200411112353530

Redis回收策略

过期判定原理:

  1. 被动访问时判定
  2. 周期轮询判定(增量)

*,目的,稍微牺牲下内存,但是保住了redis性能为王!!!!

Maxmemory配置指令

maxmemory配置指令用于配置Redis存储数据时指定限制的内存大小。通过redis.conf可以设置该指令,或者之后使用CONFIG SET命令来进行运行时配置。

例如为了配置内存限制为100mb,以下的指令可以放在redis.conf文件中。

1
maxmemory 100mb

当指定的内存限制大小达到时,需要选择不同的行为,也就是策略

回收策略

当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置。

以下的策略是可用的:

  • noeviction(默认):返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。

一般的经验规则:

  • 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。.
  • 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
  • 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。

allkeys-lruvolatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。

为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。

回收进程如何工作

理解回收进程如何工作是非常重要的:

  • 一个客户端运行了新的命令,添加了新的数据。
  • Redi检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  • 一个新的命令被执行,等等。
  • 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

近似LRU算法

Redis的LRU算法并非完整的实现。这意味着Redis并没办法选择最佳候选来进行回收,也就是最久未被访问的键。相反它会尝试运行一个近似LRU的算法,通过对少量keys进行取样,然后回收其中一个最好的key(被访问时间较早的)。

不过从Redis 3.0算法已经改进为回收键的候选池子。这改善了算法的性能,使得更加近似真是的LRU算法的行为。

Redis LRU有个很重要的点,你通过调整每次回收时检查的采样数量,以实现调整算法的精度。这个参数可以通过以下的配置指令调整:

1
maxmemory-samples 5

image-20200411112228687

Redis持久化 (重点!!!)

image-20200625213446538
RDB
image-20200411125836850
时点混乱

持久化的开始后, 还要记录现在修改的值

image-20200411122241429

管道:

Linux管道概念: 前边命令的输出作为后边命令的输入
image-20200625214137737

1,衔接,前一个命令的输出作为后一个命令的输入

2,管道会触发创建【子进程】
echo $$ | more
echo $BASHPID | more

$$ 高于 |

  • 使用linux的时候:存在父子进程
  • 父进程的数据,子进程可不可以看得到?
  • 常规思想,进程是数据隔离的!
  • 进阶思想,父进程其实可以让子进程看到数据!
  • linux中export的环境变量,子进程的修改不会破坏父进程, 父进程的修改也不会破坏子进程image-20200411123147116
写时复制
  • copy on write:内核机制

  • fork(): 系统调用

  • 写时复制, 创建子进程并不发生复制

  • 创建进程变快了

  • 根据经验,不可能父子进程把所有数据都改一遍, 玩的是指针image-20200411122355696

    要拷贝, 就是把真实数据的地址拷贝一份到需要持久化的进程中img

其实持久化进程这个时候只是指向了数据的地址, 内存消耗并不多. 如果这时候, 原来的数据修改了, 怎么办呢?

redis会开辟一块新的空间, 让写数据的地址指向新的空间
这样就不会影响持久化进程需要持久化的数据了img

创建子进程 fork(), 实现快照

8点创建子进程, 父子进程对数据的修改, 对方看不到

image-20200411124332883
RDB实现方式
  • 时点

  • save

  • bgsave

  • 配置文件给出bgsave规则

image-20200411125836850
RDB优缺点
image-20200411131300663
AOF
  • 丢失数据少
  • 4.0以后AOF是一个混合体, 重写的AOF文件的时候, Redis先用RDB写到AOF文件, 加快了重写的过程
    形成混合体文件 (默认开启)
    image-20200626082350192

image-20200411132308192

image-20200411133318275

AOF日志
image-20200411144422698

bgsave, 执行rdb

image-20200411145209929

执行bgsave 生成 dump.rdb
image-20200411145222593

无论是混合模式还是单aof, 执行bgrewriteaof, 都只保留最后的数据, 没有历史记录

*2 有两个元素
$6 描述元素的字符数
image-20200411175010233

开启混合模式 aof-use-rdb-preamble yes
执行BGREWRITEAOF
vi appendonly.aof, 出现RDB的内容

image-20200411175038779

误操作后, 只要不执行BGREWRITEAOF, 可以在日志中删除误操作记录
执行BGREWRITEAOF后, 日志就会同步成当前清空的状态

image-20200626090920441
开启, 修改配置文件

appendonly

image-20200411175528382

集群

image-20200411181020297

image-20200411181101641

image-20200411181123008

image-20200411184710813

image-20200411184729915

image-20200411200355740

image-20200411201140595

image-20200411201313150

image-20200411203600461

image-20200411203621238

image-20200411204905204

image-20200411205051120

击穿

image-20200411214604205

穿透

image-20200411214620761

雪崩

image-20200411214646499

分布式锁

基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;

Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。

Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。

Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。

Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

image-20200411215324099

Spring data Redis

pom.xml

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>

application.properties

1
2
spring.redis.host=192.168.150.99
spring.redis.port=6379

DemoApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
TestRedis redis = ctx.getBean(TestRedis.class);
redis.testRedis();
}
}

TestRedis

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.hash.Jackson2HashMapper;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class TestRedis {

@Autowired
RedisTemplate redisTemplate;

@Autowired
@Qualifier("ooxx")
StringRedisTemplate stringRedisTemplate;

@Autowired
ObjectMapper objectMapper;

public void testRedis(){

// stringRedisTemplate.opsForValue().set("hello01","china");
//
// System.out.println(stringRedisTemplate.opsForValue().get("hello01"));

RedisConnection conn = redisTemplate.getConnectionFactory().getConnection();

conn.set("hello02".getBytes(),"mashibing".getBytes());
System.out.println(new String(conn.get("hello02".getBytes())));


// HashOperations<String, Object, Object> hash = stringRedisTemplate.opsForHash();
// hash.put("sean","name","zhouzhilei");
// hash.put("sean","age","22");
//
// System.out.println(hash.entries("sean"));


Person p = new Person();
p.setName("zhangsan");
p.setAge(16);

// stringRedisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));

Jackson2HashMapper jm = new Jackson2HashMapper(objectMapper, false);

stringRedisTemplate.opsForHash().putAll("sean01",jm.toHash(p));

Map map = stringRedisTemplate.opsForHash().entries("sean01");

Person per = objectMapper.convertValue(map, Person.class);
System.out.println(per.getName());

stringRedisTemplate.convertAndSend("ooxx","hello");

RedisConnection cc = stringRedisTemplate.getConnectionFactory().getConnection();
cc.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] body = message.getBody();
System.out.println(new String(body));
}
}, "ooxx".getBytes());

while(true){
stringRedisTemplate.convertAndSend("ooxx","hello from wo zi ji ");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

MyTemplate

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MyTemplate {

@Bean
public StringRedisTemplate ooxx(RedisConnectionFactory fc){

StringRedisTemplate tp = new StringRedisTemplate(fc);

tp.setHashValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return tp ;
}
}

Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
private String name;
private Integer age ;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}

Redis的学习网站:

  1. redis.cn
  2. redis.io
  3. db-engines.com

API代码的学习:

1
2
3
1.  redis.io 的client 中有JAVA语言的客户端:jedis、lettuce等可以分别访问他们的github学习
2. 另外是基于spring的使用,主动通过spring.io官网学习spring.data.redis
3. spring.io中: https://spring.io/projects/spring-data-redis