# Redis介绍及NIO原理介绍

# 常识普及

常识:

磁盘:

1,寻址:ms级别

2,带宽:G/M

内存:

1,寻址:ns级别

2,带宽:很大

秒>毫秒>微秒>纳秒 磁盘比内存在寻址上慢了10W倍

I/O buffer:成本问题

磁盘与磁道,扇区,一扇区 512Byte带来一个成本变大:索引太多了

所以造就了4K对齐,真正使用硬件的时候,并不是真正读取512Byte,而是直接读取4K的

4K 操作系统,无论你读多少,都是最少4k从磁盘拿

# 数据库存储发展进程

首先我们知道数据可以存储在文件里,假如现有个data.txt,我们要如何查询这个文件?

比如:grep awk 还可以用java写个程序基于这个文件的IO流获取

但是随着文件的变大,相应着查询速度就会变慢,为什么呢?

因为访问硬盘时,会受到硬盘(I/O)瓶颈

如何解决查询速度变慢的问题呢?数据库出现了

数据库它解决了一件什么事情呢?

  • 有了datapage 一般是4K容量,当要读取该datapage时正好保证与操作系统一次IO(4K)相等或者大于,不要浪费IO
  • 但是光有datapage这么多个4K小格子,还是没有解决读取慢的问题,时间复杂度还和原来一样O(N)挨个去找,走着还是全量IO
  • 如何查询变快呢?建立索引,其实索引也是建立很多4K的小格子,无非前面是存储的真实的数据,索引4K小格子存储的是面向索引列(比如ID)数据ID的值所对应的datapage号,这样当查询该数据ID时,直接通过索引找到datapage,时间复杂度到O(1),不会全量IO
  • 这时有个存储数据的datapage,还要存储索引的indexpage,它们都是存储在硬盘里的,真正查询的时候还需要一个东西就是内存里准备一个B+Tree(搜索二叉树),B+Tree的树干是在内存中,所有叶子节点指向的是datapage4K小格子,为何这么设计?是因为datapage、indexpage数据量太大,内存放不下,所以内存里存着B+Tree的树干,因为硬盘有寻址慢和带宽小的缺点,所以尽可能的减少硬盘的IO次数

所以关系型数据库在建表时需要注意:必须给出schema 表的列及列的类型

类型:字节宽度

存:倾向于行级存储

假如表中有10个字段,现在只存1、7字段进去,数据库会将2~6、8~10的字段用0字节去开辟,这么做的好处是将来修改数据时,不用移动数据了,直接在位置上覆盖写

但是随着数据库中的表很大时,比如:表中有几亿行等,那么性能是否下降呢?

如果表有索引,增删改变慢,因为增删改表中数据时,数据datapage需要找到索引indexpage进行更新位置

但是查询速度呢?

1、假设我硬盘有100T,然后内存也能够将B+Tree的所有树干都能放下,那么这时候来了1个或少量查询,并且它的sql中where条件命中了索引,那么这时候查询速度依然很快

2、并发大的时候会受硬盘带宽影响速度,假设同时来了1万的sql,请求不同的datapage,系统会将datapage从硬盘读到内存,这时候受硬盘带宽影响速度

面对这种数据量大时变慢问题,数据库演化出了内存作为存储数据的极端

  • SAP HANA 内存级别的关系型数据库 2T内存存储 很贵哦
    • 数据在磁盘和内存体积不一样
    • 磁盘没有指针,也不能向内存里面进行压缩等策略
    • 假如有2T磁盘数据向内存迁移,可能内存只需要1T多

通过以上两种,企业中不可能考虑内存数据库的太贵了,但是面对关系型数据库数据量太大的时候会变慢应该怎么处理呢?全使用内存的买不起,所以还得使用磁盘的,但是磁盘的又太慢了,那么就要折中了,就是使用少一点内存将热点数据放入内存存储,读取快。所以缓存的技术就被提出了。

  • 折中方案 缓存

    • memcached

    • redis

    • 2个基础设施

    1. 冯诺依曼体系的硬件

    2. 以太网,tcp/ip的网络

# 数据库引擎介绍

架构师:技术选型、技术对比数据库的排名 (opens new window)

这个网站可以对比直接拿上面的数据,放入技术方案中。

# Redis简单介绍

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

Memcached:value没有类型的概念,那么导致返回value所有的数据到client端,而且还要解码,占用很多网卡IO

Redis:value有类型的概念,Server中对每种类型都有自己的方法。不需要全量数据,不会占用太多的网卡IO

优势就是计算向数据移动。

image-20210803100228598

# Redis安装实操总结

# 主机规划

用户名/密码 主机名称 备注 主机规划(内) 安装软件
root/root123 redis-01 Redis服务器(主) 192.168.0.188 redis5.0.5
# 安装wget
yum install -y wget
# 进入目录
cd /usr/local/
# 创建soft目录
mkdir soft
# 进入soft目录
cd soft
# 下载redis5.0.5安装包
wget    http://download.redis.io/releases/redis-5.0.5.tar.gz
# 解压安装包
tar xf redis-5.0.5.tar.gz
# 进入redis目录
cd redis-5.0.5
# 仔细看README.md
vim README.md
# 编译make
make 
# 未安装gcc 安装gcc
yum install -y gcc  
# 再次make 发现报错 执行下面清理操作
make distclean
# 最后make成功
make
# 进入源码目录 生成了可执行程序
cd src
# 返回上级目录
cd ..
# 安装redis到指定目录
make install PREFIX=/opt/yyc/redis5
# 添加Redis到环境变量
vim /etc/profile
export REDIS_HOME=/opt/yyc/redis5
export PATH=$PATH:$REDIS_HOME/bin
# 环境变量配置生效
source /etc/profile
# 验证环境变量是否生效
echo $PATH
# 进入utils目录
cd utils
# 执行install_server.sh  可以执行一次或多次 将Redis做成服务
./install_server.sh 
# a)  一个物理机中可以有多个redis实例(进程),通过port区分    
# b)  可执行程序就一份在目录,但是内存中未来的多个实例需要各自的配置文件,持久化目录等资源    
# c)  service   redis_6379  start/stop/stauts     >   linux   /etc/init.d/****     
# d)脚本还会帮你启动!
# 安装redis_6379实例
[root@redis-01 /usr/local/soft/redis-5.0.5/utils]# ./install_server.sh
Welcome to the redis service installer
This script will help you easily set up a running redis server

Please select the redis port for this instance: [6379] # 选择端口号
Selecting default: 6379
Please select the redis config file name [/etc/redis/6379.conf]  # redis配置文件
Selected default - /etc/redis/6379.conf
Please select the redis log file name [/var/log/redis_6379.log]  # redis日志输出
Selected default - /var/log/redis_6379.log
Please select the data directory for this instance [/var/lib/redis/6379] # redis数据输出目录
Selected default - /var/lib/redis/6379
Please select the redis executable path [/opt/yyc/redis5/bin/redis-server] # redis运行目录
Selected config:
Port           : 6379
Config file    : /etc/redis/6379.conf
Log file       : /var/log/redis_6379.log
Data dir       : /var/lib/redis/6379
Executable     : /opt/yyc/redis5/bin/redis-server
Cli Executable : /opt/yyc/redis5/bin/redis-cli
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6379.conf => /etc/init.d/redis_6379
Installing service...
Successfully added to chkconfig!
Successfully added to runlevels 345!
Starting Redis server...
Installation successful!
# 安装redis_6380实例
[root@redis-01 /usr/local/soft/redis-5.0.5/utils]# ./install_server.sh
Welcome to the redis service installer
This script will help you easily set up a running redis server

Please select the redis port for this instance: [6379] 6380
Please select the redis config file name [/etc/redis/6380.conf] 
Selected default - /etc/redis/6380.conf
Please select the redis log file name [/var/log/redis_6380.log] 
Selected default - /var/log/redis_6380.log
Please select the data directory for this instance [/var/lib/redis/6380] 
Selected default - /var/lib/redis/6380
Please select the redis executable path [/opt/yyc/redis5/bin/redis-server] 
Selected config:
Port           : 6380
Config file    : /etc/redis/6380.conf
Log file       : /var/log/redis_6380.log
Data dir       : /var/lib/redis/6380
Executable     : /opt/yyc/redis5/bin/redis-server
Cli Executable : /opt/yyc/redis5/bin/redis-cli
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6380.conf => /etc/init.d/redis_6380
Installing service...
Successfully added to chkconfig!
Successfully added to runlevels 345!
Starting Redis server...
Installation successful!
# 查看redis进程信息
ps -ef | grep redis
[root@redis-01 /usr/local/soft/redis-5.0.5/utils]# ps -ef | grep redis
root      21622      1  0 15:36 ?        00:00:00 /opt/yyc/redis5/bin/redis-server 127.0.0.1:6379
root      21700      1  0 15:39 ?        00:00:00 /opt/yyc/redis5/bin/redis-server 127.0.0.1:6380
root      21717   2237  0 15:40 pts/1    00:00:00 grep --color=auto redis
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

# Redis如何抗住并发及NIO原理

Redis是单进程、单线程、单实例的,那么它是如何抗住并发的?并发很多的请求,如何变得很快呢?

科普一些知识:Linux中有Kernel内核程序,当Client与服务器中Linux建立连接,那么一定是先到达Kernel中,创建很多的Socket,这时Redis与Kernel之间使用的epoll(事件驱动)非阻塞多路复用器,epll是Kernel提供一种系统调用,也就是说Redis可以用单线程通过系统调用遍历这么多连接,谁有数据我就处理谁。Redis的NIO原理

查看文件描述符:

ps -ef | grep redis
[root@redis-01 /usr/local/soft/redis-5.0.5/utils]# ps -ef | grep redis
root      21622      1  0 15:36 ?        00:00:09 /opt/yyc/redis5/bin/redis-server 127.0.0.1:6379
root      21700      1  0 15:39 ?        00:00:09 /opt/yyc/redis5/bin/redis-server 127.0.0.1:6380
root      21816   2237  0 18:13 pts/1    00:00:00 grep --color=auto redis
# 进入21622这个进程下查看文件描述符目录
cd /proc/21622/fd
ll
[root@redis-01 /proc/21622/fd]# ll
total 0
lrwx------. 1 root root 64 Aug  9 18:15 0 -> /dev/null
lrwx------. 1 root root 64 Aug  9 18:15 1 -> /dev/null
lrwx------. 1 root root 64 Aug  9 18:15 2 -> /dev/null
lr-x------. 1 root root 64 Aug  9 18:15 3 -> pipe:[48038]
l-wx------. 1 root root 64 Aug  9 18:15 4 -> pipe:[48038]
lrwx------. 1 root root 64 Aug  9 18:15 5 -> anon_inode:[eventpoll]
lrwx------. 1 root root 64 Aug  9 18:15 6 -> socket:[42643]
# 0->标准输入 1->标准输出 2->错误输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

简单说一下IO演化历程:

计算机有Kernel内核,有很多的Client连接,所有连接先到达内核,早先是多个线程里每个线程通过read命令读取文件描述符,socket读取文件描述符,若文件描述符里的数据没有到的时候,read命令就不能返回一直阻塞的,因为这个时期socket是阻塞的blocking的这个时期是BIO,因为是阻塞的所以需要每个连接开辟一个线程,但是CPU在任意时刻只能执行一个线程,当数据到达时真正需要执行的线程很可能抢不到CPU时间片,会造成浪费,而且线程数太多会造成CPU切换资源增加,性能低下,这样计算机硬件是很难利用起来的,接下来应对这种问题内核发生了变化,通过man 2 socket查看socket的系统调用会发现SOCK_NONBLOCK可以开启同步非阻塞模式,所以对应着文件描述符就是非阻塞的了,那么非阻塞了就可以使用一个线程处理一个while死循环read一批文件描述符,询问有无数据,有数据则处理,没有数据就轮询下一个,轮询发生在用户空间,这时是同步非阻塞时期也就是NIO;但是这种模型有个问题:如果有1000个文件描述符,代表用户进程轮询调用1000次Kernel调用成本问题,内核态与用户态来回切换;如何解决这个问题:就是减少Kernel调用次数,这就造成Kernel要向前发展;就是将轮询多个文件描述符是否有数据这件事交由Kernel处理,由此衍生出了select系统调用,通过man 2 select查看select的系统调用,统一将1000个文件描述符传给select将有数据的文件描述符返回,然后程序再将这些文件描述符遍历调用read(fd),这样就形成最初期的多路复用器,减少内核态与用户态的切换,提高效率;但是这种还是有些复杂,因为我每次要传入1000个fds然后返回有数据的fds再遍历read(fd),这个过程中1000个fds在用户态与内核态中来回copy,影响性能,能不能有个系统调用解决此问题呢? 就是mmap,通过man 2 mmap查看mmap的系统调用,mmap能够在用户态与内核态开辟一个共享空间,这样能够减少用户态与内核态来回copy的操作。

eventpoll

用户态中程序将1000个fds放入mmap共享空间的红黑树中,内核态中也能看到这个红黑树,当有数据到达时,发生IO中断,将有数据到达的fds放入链表中,那么用户态中程序获取链表中的fds进行read读取操作。

零拷贝sendfile

之前程序要想将本地磁盘文件file.txt发送到网络上,操作如下:

  1. IOSocket的文件描述符fd4,文件Socket的文件描述符fd3,Kernel要read(fd3),然后write(fd4),Kernel需要将fd3这个copy到用户态,然后用户态写入fd4,再将fd4copy到内核态中;
  2. 有了sendfile,直接系统调用sendfile(out, in)实现在内核态的处理,不用再内核态与用户态之间的copy了。

NIO原理及发展历程