深度长文整理-Redis进阶(一)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 深度长文整理-Redis进阶(一)

一、基础#


重新整理了一下,这篇笔记之前还有一篇基础相关的笔记,点击进入



二、为什么Redis是单线程的?#


官方回答:

Redis是基于内存操作,CPU不是Redis的性能瓶颈,Redis的性能瓶颈是机器的内存大小、以及网络的带宽,既然单线程容易实现,那就直接使用单线程来实现了


此外:

使用单线程实现,那所有的命令就会排队执行,不需要考虑各种同步问题和加锁带来的性能消耗问题。

既然CPU不是Redis的瓶颈,那么如果不想让服务器的其他CPU闲置,可以考虑起多个Redis进程,因为Redis不是关系型数据库,数据之间也没有约束。这样还能搭建集群,分压分流。


三、为什么单线程这么快?#


  1. Redis是一款内存数据库,基于内存的读写速度本来就很快
  2. 如果使用多线程的话会有线程上下文的切换。对于内存系统来说,单线程操作内存的效率才是最高的。
  3. Redis使用了epoll IO多路复用,可以实现用一条线程处理并发的网络请求


四、select、poll、epoll#


select、poll、eopll是操作系统处理网络上传输过来的数据的不同实现,数据从经过网线流入网卡,网卡中的驱动程序会向CPU发出中断信号,在交互系统中,中断信号的优先级是很高的,CPU立刻去处理这个中断信息,CPU通过终端表找到相应的处理函数:


1、禁用网卡的中断信号,告诉网卡下次有数据过来直接写内存就ok

2、通过驱动程序申请、初始化一块内存,将网卡中的数据写进内存中

3、然后解析处理数据:操作系统先校验数据是否符合os structure、数据往上层传递,

Ehthernet校验数据是否符合预期的格式,继续向上层传递到ip层,再往上到tcp/udp层并按照指定的协议去解析

4、应用层想使用这部分数据就有一个拆包+格式校验的过程

内存指的socket文件的接受缓冲区。


作为一个网络服务器同一时刻可能有多个socket和他建立连接与他进行数据的交互,这里的select、poll、epoll说的其实就是在众多的socket中如何快速高效的找到接受缓冲区存在数据的socket文件,然后交给应用层的代码去处理它


Select模型



操作系统为每一个Tcp连接都会相应的创建sock文件,这些sock文件隶属于操作系统的文件列表。

当sock收到了数据,会调用中断程序唤醒进程A,将进程A从所有的Sock的等来队列中移除,加入到内核空间的工作队列中进程A只知道至少有一个sock的接受缓冲区已经由数据了,但是它不知道到底是哪个sock,所以它得通过遍历sock列表的方式找到这个sock。


select的缺点和不足:

  1. 进程A需要添加进所有的sock的等待队列中,这会进行一次遍历。
  2. 当有sock就收到数据时,又得将进程A从所有的sock等待队列中移除,这又是一次遍历。
  3. 进程A寻找有数据的sock时,还会发生一次遍历。
  4. 为了放置单个进程将系统的所有资源都耗干,linux会限制单个进程能打开的fd文件句柄数,即使你可以修改配置,突破这会个限制


下图截自《UNIX环境高级编程》第二版



上图可以看到,使用select系统调用上有三个核心参数:分别是 readfds、writefds、exceptfds指向文件描述符号指针,每个描述符都被放在fd_set中, 也就是说针对read、write、和except分别对应着一个独立的fd_set , (并没有网上流传的数组哦,至少《UNIX环境高级编程》是这样讲的)

下图是截取自《UNIX环境高级编程》的关于fd_set的相关信息,fd_set 是一个bit mask,不是数组。



Poll模型


网上一直流传着这样一句话:poll本质上和select没有区别,都会进行好几次无谓的遍历才能找到到底是那个sock文件的接受缓冲区中接受到了数据。

下图是我截自《UNIX环境高级编程》关于poll部分的内容



书中关于poll的描述,poll模型中定义了一个pollfd,对fd进行了封装,也就是说,poll是使用数组来保存fd的,就是上图中的pollfd数组

网上流传的另一个版本就是说:poll使用链表维护着fd,所以poll没有最大连接数的限制,这一点有待证实,至少《UNIX环境高级编程》中对链表的事只字未提

从书中的描述看,poll确实是用数组来维护fd的,并且还自己封装了个pollfd,维护的是pollfd数组,那为什么poll没有连接数的限制呢?


我是这样理解的:select之所以受到能仅仅能打开1024的限制,是因为操作系统层面上默认就有对单个进程能打开fd的作出的限制,比如32位的OS默认就是1024。那我用poll同也会受到这个1024的限制,但是我能修改这个限制,让他变得比1024大。比如改成10万(只要你的服务器性能够好就行,数组中就能存更多的fd,遍历处理起来就更快)。所以这才会说,poll理论上可以没有限制。


当然我上面说的不一定就对,如果你有更好的解析,欢迎留言。


Epoll模型



Epoll的设计目标就是优化掉Select 和 Poll模型中查找接收到数据的sock文件时进行的无谓的遍历操作。


看上图:在select模型中,需要将进程添加进每一个sock的等待队列,然后阻塞,假如10万TCP连接对应着10万个sock文件,那这个添加+阻塞的操作就得重复10万次

对于epoll来说可以看到,这个添加的过程只进行了一次...见下图



int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
    int n = epoll_wait(...)
    for(接收到数据的socket){
        //处理
    }


当执行系统调用 epoll_create(...) 内核会创建上图中的eventpoll对象,eventpoll对象也隶属于操作系统的文件系统,此外所有的sock都注册在eventpoll中。


进程不再注册在每一个sock的等待队列中,而是注册在eventpoll的等待队列中,此外,接受缓冲区存在数据的sock会被注册进eventpoll的rdlist中。这样当进程再次被唤醒添加到操作系统的工作队列中时,从eventpoll的rdlist中就能确切的获取到哪些sock是需要处理的sock,免去了遍历之苦。


eventpoll中的数据结构


rdlist: 里面存放就绪列的socket,为了满足快速方便删除、添加。它被设计成了双向链表

epoll中也是需要保存受监视的sock,为了方便添加、搜索、检索。被设计成红黑树。因为它的搜索、插入、删除的时间复杂度都是O(logN)


Epoll的连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。


参考:《UNIX环境高级编程》

参考:(这个大佬讲的超级好):https://zhuanlan.zhihu.com/p/63179839


五、Redis的事务#


原子性:一组命令要么同时成功,要么同时失败


但是redis中的每一条单独的命令是有原子性的,但是Redis中的事务不能保证原子性

redis中的事务没有隔离级别的概念,不可能出现脏读、幻读、不可重复读

在redis中,事务的本质是一组命令的集合,一个事务中的所有命令都会有被序列化,在事务执行的过程中:顺序、排他、一次性执行。


Redis事务的过程:

  • 开启事务
  • 一连串普通命令
  • 执行事务


# 开启事务
127.0.0.1:16379> MULTI
OK
# 添加命令
127.0.0.1:16379> SET k1 v1
QUEUED
127.0.0.1:16379> SET k2 v2
QUEUED
# 执行事务
127.0.0.1:16379> EXEC
1) OK
2) OK
127.0.0.1:16379>
# 开启事务
127.0.0.1:16379> MULTI
OK
# 添加命令
127.0.0.1:16379> set k3 v3
QUEUED
127.0.0.1:16379> SET k4 v4
QUEUED
# 取消事务
127.0.0.1:16379> DISCARD
OK
# 检查结果,确实没有执行刚刚添加的命令
127.0.0.1:16379> keys *
1) "k1"
2) "k2"
127.0.0.1:16379>


假设开启时候后,多条命令中有一个命令出现运行时异常有什么影响?


出现异常的命令不会被执行,但是这个异常的命令不会影响它后面的命令执行,因为这个原因我们说redis的事务不支持原子性


# k1的值为字符串
127.0.0.1:16379> set k1 "v1"
OK
# 开启事务
127.0.0.1:16379> MULTI
OK
# 设置事务的值
127.0.0.1:16379> set k2 v2
QUEUED
# 对字符串类型的值+1,会抛出运行时异常
127.0.0.1:16379> INCR k1
QUEUED
# 继续添加两个值
127.0.0.1:16379> set k3 v3
QUEUED
127.0.0.1:16379> set k4 v4
QUEUED
# 执行事务,看到,运行时异常的命令不会影响后续的命令执行
127.0.0.1:16379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) OK
127.0.0.1:16379>


假设开启时候后,多条命令中有一个命令出现编译异常有什么影响?

出现编译型异常,所有的命令都不会被执行


# 开启事务
127.0.0.1:16379> MULTI
OK
# 往命令队列中添加命令
127.0.0.1:16379> set k1 v1
QUEUED
127.0.0.1:16379> set k2 v2
QUEUED
# 故意添加一个语法错误的命令,导致编译异常
127.0.0.1:16379> GETSET k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:16379> set k4 v4
QUEUED
# 执行事务
127.0.0.1:16379> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 检查结果,发现所有的命令都没有被执行
127.0.0.1:16379> keys *
(empty list or set)
127.0.0.1:16379>


CAP理论

nosql同样也有一套属于自己的CAP

  • C(Consistency 强一致性)
  • A(Availability可用性)
  • P(Partition tolerance分区容错性)


CAP 的理论核心是: 一个分布式的系统,不可能很好的满足一致性,可用性,分区容错性这三个需求,最多同时只能满足两个.因此CAP原理将nosql分成了三大原则:


  • CA- 单点集群,满足强一致性和可用性,比如说oracle,扩展性收到了限制
  • CP- 满足一致性,和分区容错性Redis和MongoDB都属于这种类型
  • AP- 选择了可用性和分区容错性,他也是大多数网站的选择,容忍数据可以暂时不一致,但是不容忍系统挂掉


六、Redis的监控#


redis可使用watch监视某一个key,然后开启事务操作某一个key,当key没有发生异常变动时,事务正常结束


一旦事务成功执行后,watch就会自动取消掉


127.0.0.1:16379> set money 100
OK
127.0.0.1:16379> set out 0
OK
# 监视key
127.0.0.1:16379> WATCH money
OK
127.0.0.1:16379> MULTI
OK
127.0.0.1:16379> DECRBY money 20
QUEUED
127.0.0.1:16379> INCRBY out 20
QUEUED
127.0.0.1:16379> exec
1) (integer) 80
2) (integer) 20


下面演示一个出现异常的例子:

事务中,添加watch的key被修改后,执行事务返回nil,表示失败

验证了watch机制使用的是乐观锁机制



当遇到上面这种返回nil的情况下,可以像下面这样处理


# 取消监视(解锁)
127.0.0.1:16379> UNWATCH
OK
# 重新监视
127.0.0.1:16379> watch money
OK
# 重新开启事务
127.0.0.1:16379> MULTI
OK
127.0.0.1:16379>


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
|
6月前
|
NoSQL Java Redis
Redis进阶-lua脚本
Redis进阶-lua脚本
113 0
|
6月前
|
存储 NoSQL Java
Redis进阶-细说分布式锁
Redis进阶-细说分布式锁
75 0
|
6月前
|
NoSQL Redis
Redis进阶-bind参数详解
Redis进阶-bind参数详解
402 0
|
6月前
|
NoSQL Java Redis
Redis进阶-Jedis以及Spring Boot操作 Redis 5.x Cluster
Redis进阶-Jedis以及Spring Boot操作 Redis 5.x Cluster
106 0
|
6月前
|
NoSQL 算法 Redis
Redis进阶-Redis对于过期键的三种清除策略
Redis进阶-Redis对于过期键的三种清除策略
102 0
|
5月前
|
NoSQL 数据可视化 Java
rodert单排学习redis进阶【白银一】
rodert单排学习redis进阶【白银一】
27 0
|
5月前
|
NoSQL Redis 数据库
rodert单排学习redis进阶【青铜】2
rodert单排学习redis进阶【青铜】
32 0
|
5月前
|
缓存 NoSQL Java
rodert单排学习redis进阶【青铜】1
rodert单排学习redis进阶【青铜】
39 0
|
5月前
|
NoSQL 关系型数据库 MySQL
Redis进阶-select 1. /xxx 切换数据库DBSIZE- 获取当前数据库中的key的个数flushdb-删除当前数据的所有keyflushall-删除所有表的所有库Re
Redis进阶-select 1. /xxx 切换数据库DBSIZE- 获取当前数据库中的key的个数flushdb-删除当前数据的所有keyflushall-删除所有表的所有库Re