Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

评价标准:

  1. 均衡:每一个worker工作负载应该是相对均衡的
  2. 对于接受的连接请求,不可以出现饿死现象
  3. 不能有明显的瓶颈
  4. 主要耗时与存取队列耗时,占比要尽量打,让CPU更多的时间做事情,而不是做辅助。

单线程模型

代表:Redis

基本原理:

单一线程,依次socket->bind->listen,然后epoll_wait分别进行accept以及读写事件

优点&使用场景:

简单、没有并发锁问题;适用于短耗时的计算密集型服务

缺点:

不能支持耗时较长的事件,尤其是IO密集型

单线程(listen+accept+epoll_wait) + 1队列通知 + n线程(读写处理) 模型

代表:thrift-nonblocking-server

基本原理:

  1. 在这种模型中,有1+n个线程
  2. 有1个线程执行端口的listen并把listen_fd加入该线程的epoll_set,然后循环去做如下事情:

    2.1 epoll_wait监听新连接的到

    2.2 调用accept获得新到的fd

    2.3 把fd放入队列

    回到2.1,继续epoll_wait

  3. 另外有n个工作线程,从队列里面获取文件描述符,然后执行:1)读取数据,2)执行任务

优点:

  • 模型不算复杂
  • 并发能力强,能够充分利用多核
  • 天然支持负载均衡(每个工作工作线程完成任务之后就会去队列里主动获取文件描述符)

缺点:

队列可能是性能瓶颈,尤其是当业务逻辑耗时本身极其短的情况下

单线程(listen+accept+epoll_wait) + n队列通知 + n线程(读写处理) 模型

代表:memcache

基本原理:

  • 这种模型基本类似于上一种模型,区别在于把1个队列换成n个队列,每个工作线程绑定一个队列,每个工作线程从自己的队列消费数据,其他的保持一致

  • LISTEN线程往PIPE里写入一个哨兵,通知WORKER线程队列可读

优点:

并发能力更强。相比于单队列的模型,多队列的好处是减少了队列的锁竞争。对于短耗时任务能得到比较多的提升,很适合缓存类应用

缺点:

有可能导致负载不均。因为监听线程是不会去根据不同线程的处理速度决定把任务分配给哪个线程的,如果每个任务的耗时不均衡,那么就可能导致有些线程累死,有些线程饿死。

单线程listen, 在处理高速率海量连接时,单线程可能会成为瓶颈

单进程(listen) + n进程(accept + epoll + 读写处理) 模型

代表:nginx

基本原理:

  1. master进程监听新连接的到来,并让其中一个worker进程accept。这里需要处理惊群效应问题(加锁、SO_REUSEPORT)
  2. worker进程accept到fd之后,把fd注册到到本进程的epoll句柄里面,由本进程处理这个fd的后续读写事件
  3. worker进程根据自身负载情况,选择性地不去accept新fd,从而实现负载均衡

单线程(listen) + n线程(accept + epoll + 读写处理 + 协程) 模型

主线程

具体线程

====

Reactor 模型就是网络服务器端用来处理高并发网络 IO 请求的一种编程模型.

  1. 同时只有一个worker来accept接受新的连接请求。一个连接上的所有请求都是由同一个worker来处理。

  2. 通过iovec来接收输入, iovec可以直接交给pb来解析。

  3. 如果是一个新的请求包,则开启一个co由它来进行处理。

  4. 如果是一个回复包,则根据回复包的seqno, 查询map, 找到对应的co, co_resume, 执行原来的业务逻辑。

  5. 通过时间轮来管理所有的超时时间。

  6. epoll_wait的timeout 是 min(1ms, 下次超时时间)

  7. 通过pipe来进行线程同步。

高速缓存服务器(Cache Server)是软硬件高度集成的专业功能服务器,主要做高速缓存加速服务,一般部署在网络边缘。

根据加速对象不同,分为客户端加速和服务器加速

客户端加速Cache部署在网络出口处,把常访问的内容缓存在本地,提高响应速度和节约带宽;

服务器加速Cache部署在服务器前端,作为Web服务器的前置机,提高Web服务器的性能,加速访问速度。


缓存有哪些类型?

  • 服务器LocalCache (有状态)

将缓存内容放到服务器的本地内存或者磁盘;

无法及时更新,一般都是设置一个合理的过期时间,让其自动过期;适用于实时性要求不高的场景;

优缺点:本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

特例:数据库缓存

  • 分布式缓存(无状态)

redis\memcache\tair

  • 客户端缓存(有状态)

具体有哪些:
浏览器cookie;浏览器本地缓存;flash本地存储;html5的本地存储;native app 本地缓存

缓存系统对比与选型


在Web应用领域,Web缓存大致可以分为以下几种类型:

浏览器端缓存

浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果你浏览过程中,比如前进或后退,访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显现。

客户端浏览器缓存主要是通过在http头部增加
Last-Modified,If-Modified-Since,Expires,Cache-Control等标识,和服务器进行协商,是否是采用客户的本机缓存来实现。

其中这里面也会分为三种方式

1 通过Last-Modified,If-Modified-Since方式和服务器通信,客户发出http请求中包含If-Modified-Since,如果服务器端代码没有修改,服务器端返回302响应代码的请求响应头(内容不返回)客户端则直接用本机缓存的内容缓存显示结果。相
当于节省了服务器执行代码时间以及数据传输时间。

2 通过Expires,Cache-Control控制,客户端发现如果上次请求的页面还未过期,通过Expires或者Cache-Control进行辨别,则直接显示本机缓存的内容,不与服务器进行通信。

服务器端缓存

CDN缓存

CDN(Content delivery networks)缓存,也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器,从这个层面来说,本文讨论浏览器和服务器之间的缓存机制,在这种架构下同样适用。

代理服务器缓存

代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少相应时间和带宽使用方面很有效,同一个副本会被重用多次。常见代理服务器缓存解决方案有Squid等

Web应用层缓存

应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据,页面,图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。

数据库数据缓存

Web应用,特别是SNS类型的应用,往往关系比较复杂,数据库表繁多,如果频繁进行数据库查询,很容易导致数据库不堪重荷。为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返回,提供响应效率。比如常用的缓存方案有memcached等。

总结一下:
一般的高并发的应用程序,都在web层采用了以上几种缓存,一般静态资源(图片,js,css)都会采用nginx反向代理+客户端缓存来实现
对于门户网站,尤其是首页的新闻,一般都会缓存起来,可以通过反向代理也可以通过应用程序缓存实现方式
对于下载或者视频网站,由于数据传输比较大,直接采用浏览器本地缓存实现。


业务需求可能涉及的缓存组件要求:

容量

需要存储什么类型的内容? 存储量多大?

并发(qps)

缓存的读写比例以及qps如何?

响应时间

更高的qps可以通过扩容来解决,但是一次响应的时间是有限制的,例如跨机房访问延迟0.5ms,就决定了响应时间不可能低于0.5ms;

反过来,如果业务上要求的resp time必须小于0.5ms, 那么该缓存就一定满足不了我们的要求。

使用成本

分为两部分:

  1. 首先服务端,主要包括:运维成本、机器成本
  2. 第二,客户端,主要包括程序员研发成本:单一的库依赖、简洁的配置和人性化的API丰富的文档和技术支持
扩展性要求

在某方面出现瓶颈(qps或者容量)时,能否通过增加机器来快速在线扩容;这个主要涉及到系统的负载均衡能力;

容灾能力

缓存数据丢失;不同的系统有不同的容灾能力;一定要结合业务需求

memcached作为高速运行的分布式缓存服务器,具有以下的特点:

(1)协议简单,文本协议

(2)基于libevent的事件处理,对服务器的连接数增加,也能发挥O(1)的性能

操作:

(1)set, get, del

(2)多key查询

(3)原子自增,自减

https://www.codedump.info/post/20210812-memcached/

memcache内存分配

Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。但是存在内存浪费问题!!(外碎片没有了,但是当分配大小超过实际要用大小时,产生内部碎片)

memcache删除机制

Lazy Expiration

基于LRU(Least Recently Used)算法自动删除不使用的缓存

memcached内部不会监视记录是否过期,而是在get时查看记录的时间戳,检查记录是否过期。这种技术被称为lazy(惰性)expiration。因此,memcached不会在过期监视上耗费CPU时间。

当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期 key 进行清理,还会按 LRU 策略对数据进行剔除。

1.4以前:

维护一个双向链表,当被访问时,移动到head; 当需要淘汰时,从尾巴开始扫描(也就是从旧到新),找到已经过期的item淘汰掉。

弊端1:(过期的因为被访问过,没法被尽快淘汰)元素被访问一次就会被放到LRU链表的头部,这样即便这个元素可以被淘汰,也会需要很久才会淘汰掉这个元素。
弊端2:(未过期的,因为长时间没被访问所以比较靠后)由于上面的原因,从链表尾部开始找可以淘汰的元素时,实际可能访问到的是一些虽然不常被访问,但是还没到淘汰时间(即有效时间TTL还未过期)的数据,这样会一直沿着链表往前找很久才能找到适合淘汰的元素。由于这个查找被淘汰元素的过程是需要加锁保护的,加锁时间一长影响了系统的并发。

1.5以后:

维护分段LRU(HOT\WARM\COLD),有点类似多级队列。
总体思路就是根据访问频率,完成一个元素在三个队列中的移动。

哈希算法

memcached不互相通信的分布式,由客户端来实现

根据余数计算分散

数据的分散性也相当优秀,但也有其缺点。那就是当添加或移除服务器时,缓存重组的代价相当巨大。
添加服务器后,余数就会产生巨变,这样就无法获取与保存时相同的服务器,从而影响缓存的命中率。

一致性Hash

但Consistent Hashing中,只有在continuum(统一连续体)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。Consistent Hashing最大限度地抑制了键的重新分布。

而且,有的Consistent Hashing的实现方法还采用了虚拟节点的思想。使用一般的hash函数的话,服务器的映射地点的分布非常不均匀。

因此,使用虚拟节点的思想,为每个物理节点(服务器)在continuum上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。

hash表扩容

基于双缓冲思想的扩容方案

启动一个后台线程,监控hash表大小,如果快满了,则拷贝到新的hash表【扩大容量】

扩容步骤为:

按照新的大小分配出来新的hash数组保存到primary_hashtable中,原hash数组命名为old_hashtable,另外有扩容索引值expand_bucket,在该索引之前的数据,表示已经从old_hashtable中转移到新的hash数组了。
每次操作一个hash桶元素,需要对该hash桶对应的item锁进行加锁之后才能开始转移。
期间如果有数据访问,首先按照旧的hash桶数量进行计算,如果计算出来的索引值不小于expand_bucket,说明这个数据还在旧的桶里,到old_hashtable中查找;否则说明在新的hash数组里了,按照新的hash桶数量计算器索引值,然后再到primary_hashtable中操作。

事务的特性
  • A 原子性(Atomicity),要么执行要么不执行
  • C 一致性(Consistency),事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。(事务的acid不是完全正交的,尤其是一致性,可能跟原子性、隔离性都有一定关系。一致性是一个更宏观的要求。)
  • I 隔离型(Isolation),数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable
  • D 持久性(Durability),一旦事务提交,对数据的改变就是永久的,即便系统故障也不会丢失

事务同时运行可能出现的问题

  • 脏读,事务B读到事务A还没有提交的数据
  • 不可重复读,一行被SELECT两次,返回的结果不一样
  • 幻读,两次读取返回的集合不一样
事务的四种隔离级别
  • 读未提交,在该隔离级别,会出现脏读、不可重复读、幻读等问题。
  • 读已提交,该隔离级别解决了脏读的问题,依旧会出现不可重复读、幻读问题。
  • 可重复读,该隔离解决进一步解决了不可重复读的问题,会出现幻读问题。(但是对于InnoDB存储引擎,通过间隙锁解决了该问题,不会出现幻读现象)
  • 串行,该隔离级别把所有操作都串行化了,没有并发访问,解决了以上所有问题。

数据库各隔离级别会出现的问题

Mysql_InnoDB引擎各隔离级别会出现的问题

虽说希望你了解,但是友情提示一波:线上高并发应用,尽量不要用事务

数据库索引一些经常考察的知识点

mysql数据库索引

  1. Mysql 数据库有哪些索引以及他们各自的特点?InnoDB为什么选择用B+树作为索引,而不用B树?
索引 特点 使用场景
hash索引 散列表实现,等值查询效率高,不能排序,不能进行范围查询 不需要范围查询,仅需等值查询时,可以考虑使用
BTree索引 B+树实现,支持范围查询 默认
RTree索引 仅支持地理位置类型,RTree空间树实现,相比Btree有更好的范围查询性能 有按照地理位置检索需要的场景
FullText索引 分词加倒排索引实现 有类似like的全文检索类型的查询

三、哈希索引的优势:
等值查询,哈希索引具有绝对优势(前提是:没有大量重复键值,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。

四、哈希索引不适用的场景:

  • 不支持 范围查询
  • 不支持索引完成排序
  • 不支持联合索引的最左前缀匹配规则

相比B树,B+树索引支持范围查找。

索引匹配原则:左前缀匹配原则

聚簇索引、非聚簇索引

索引 特点 使用场景
聚簇索引(clustered index) 数据按照索引顺序存储,叶子节点存储真实的数据 InnoDB索引
非聚簇索引(secondary index,non-clustered index) 叶子节点存储指向真正数据行的指针 MyISAM

聚簇索引与非聚簇索引

数据库回表

数据库回表是怎么回事?如何避免?

在查询辅助索引时,如果要查询的字段已经全部在索引中了,那么就不需要额外再查询主索引了;反之,如果要查询的字段当前索引无法覆盖,那么Mysql需要额外查询主索引去获取要查询的字段,访问索引的次数多了一次,我们称刚才的过程为回表。我们通过增加全覆盖索引可以避免回表。

InnoDB索引与MyISAM索引的区别
索引 特点 使用场景
InnoDB索引 InnoDB的主索引的叶子节点就是数据本身,而辅助索引的叶子节点是主键ID InnoDB索引
MyISAM索引 InnoDB的主索引与辅助索引没有区别,叶子节点存储都是指向真实数据行的指针 MyISAM
  1. 索引为什么要用B+树来实现?
    首先,相比红黑树、AVL等二叉平衡树,B+树更加矮胖,这样子索引查找便能够更好的访问磁盘IO,从而有更好的查询性能;另外相比B树,B+树在叶子节点之间维护了一根链表,借助该链表,范围查找性能更加稳定。

    MysqlB+树索引展示

  2. 两种存储引擎区别与各自使用场景

存储引擎 特点 使用场景
MyISAM 不支持外键,表锁,插入数据时锁定整个表,查表的总行数不需要扫表,索引与数据分开 不需要支持事务,绝大多数请求为读操作,系统崩溃后数据丢失可接受
InnoDB 支持外键,行级锁,事务,查表的总行数时需要扫表,必须有唯一索引,索引与数据在一个文件中 需要支持事务,读写相当,不可接受数据丢失
  1. 为什么说数据表超过2000W就会变慢?有理论依旧吗?

磁盘扇区、文件系统、InnoDB存储引擎都有各自的最小存储单元。

最小存储单元

非叶子节点由索引值和指针构成:主键假设8字节;指针8字节;所以一个页最多有多少个指针呢? 16k / 16 = 1000左右。

叶子节点直接存数据,假设数据大小为1k,那么一个叶子节点存了16条记录。

所以,B+树树高为1的话,存的记录最多为16 * 000; 高为2的话,1000 * 1000 * 16 = 1600w左右。

在内存有限的情况下,多加一层,就意味着要多一次磁盘IO,性能便会急剧下降。

自增ID

1、一张表,里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记录的 ID 是 18 还是 15 ?

(1) 如果表的类型是 MyISAM,那么是 18

因为 MyISAM 表会把自增主键的最大 ID 记录到数据文件里,重启 MySQL 自增主键的最大ID 也不会丢失

(2)如果表的类型是 InnoDB,那么是 15

InnoDB 表只是把自增主键的最大 ID 记录到内存中,所以重启数据库或者是对表进OPTIMIZE 操作,都会导致最大 ID 丢失

字符串索引

还有没有其他方式帮助字符串建立索引

比如能够给确定业务需求里面只有按照身份证等值查询的需求,需要给身份证加索引,有没有什么办法,占用更小空间,也能达到相同的查询效率。

第一种方式是使用倒序存储

【基础假设】存储的字符串后半部分的区分度相对更高

身份证最后 6 位,没有重复逻辑,因此最后 6 位可能提供了足够的区分度。

先倒序存储,然后再创建前缀索引。

如果存储身份证的时候倒过来存,每次查询的时候,可以这样:

select field list from t where id card reverse(‘input id card string);
第二种方式使用 hash 字段

可以使用表上再创建一个整数字段,来保持身份证的校验码,同时在这个字段创建索引。

alter table t add id card crc int unsigned, add index(id_card_crc);
每次插入新记录的时候,都同时使用 crc32 这个函数 得到校验码填到这个新字段,校验码可能存在冲突,也就是两个不同的身份证通过 crc32() 函数得到的结果可能是相同的,查询要查询语句 where 部分判断 id_card 的值是精确相同的。

排序

全字段排序 VS row_id排序

全字段排序与row_id排序的区别在于:sortbuffer中存放了啥; 回表查询的次数不一致
对于全字段排序,sortbuffer会存放所有数据,然后在sortbuffer中排序,

对于 InnoDB 表来说,在内存足够的情况下,会优先选择全字段排序的方式。在内存不足的情况下,可能会借用外部文件进行排序。

但如果单行内容较大时,会导致拆分的外部文件过多,进行归并排序时,效率变低。此时会采用 Row_id 的排序方式。

对于 Memory 表来说,会优先选择 Row_id 的排序方式。

MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。 对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

如果数据量很大,内存中无法存下这么多,就会使用磁盘临时文件来辅助排序,称为外部排序;
外部排序,MySQL会分为好几份单独的临时文件来存放排序后的数据,一般是磁盘文件中进行归并,然后将这些文件合并成一个大文件;

orderby 排序内部原理
全字段排序
为避免全表扫描,我们需要在 相关字段 字段加上索引。创建索引之后,我们用 explain 命令查看排序语句的执行计划。
Extra 这个字段中存在“Using filesort”表示的就是需要文件排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。
sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。
number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解,MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。
如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。
否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。
rowid排序
全字段排序方法缺点:单行大的话占用内存空间。通过修改MySQL中专门控制用于排序的行数据的长度的一个参数。意思是,如果单行的长度超过这个值,就会换一种算法
rowid 方式和全字段方式一样,需要先把查询到的结果全部放在内存或硬盘中,再使用相关算法进行排序。而排序后由于没有保存所需的字段,需要按顺序使用主键再从索引树上查询,查到一个就返回一个,而不用把所有内容查完放到内存上再一并返回。
全字段排序 VS rowid 排序
如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。
对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。
小结
其实,并不是所有的 order by 语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的。创建联合索引等可以使其选择出来的就是按照索引字段排序的。但是,不能为了每个查询能用上覆盖索引,就要把语句中涉及的字段都建上联合索引,毕竟索引还是有维护代价的,这是一个需要权衡的决定

参考:
https://blog.csdn.net/qq_29066329/article/details/90036836

我们通过几个问题来介绍InnoDB存储引擎

LBCC VS MVCC

Lock Based Concurrency Control(LBCC)

保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。

MVCC Multi Version Concurrency Control(MVCC)

MVCC是InnoDB存储引擎为了实现事务的隔离级别而引入的一种乐观锁机制。
如果要让一个事务前后两次读取的数据保持一致,那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。

MVCC的目的在于:我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。

Undo Log

Undo log: 是什么? 通过它解决了什么问题? 数据的多个版本,临时写在undo log中,并通过链表管理起来。

InnoDB为数据库中的每一行添加了三个隐藏字段:DB_TRX_ID(事务版本号)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏ID)。

  • DB_TRX_ID:记录了创建/更新这条数据的事务版本号(版本号会递增)。
  • DB_ROLL_PTR:记录了一个指向undo log中历史版本的数据指针。(用来支持回滚操作)
  • DB_ROW_ID:一个自增的隐藏行ID。

InnoDB基于事务版本号、回滚指针这两个字段,可以在undo log中形成一个单向链表,最新版本的数据放在链表头部,历史数据通过DB_ROLL_PTR指针进行关联。如下图所示

undo list

MVCC

一致性读视图包括:视图数组(活跃的事务) + 高水位(已经创建过的事务ID + 1)

InnoDB在事务开启后执行第一个查询时,会创建一个快照(下文称之为ReadView),这个ReadView包含了以下信息

  • m_ids: 活动事务id列表(活动事务指的是已经开始、尚未提交/回滚的事务)
  • min_trx_id: 最小活动事务id
  • max_trx_id: 最大活动事务id
  • creator_trx_id:当前事务id

紧接着InnoDB会通过查询语句定位到最新版本的数据行,并根据以下规则获取到可以访问的数据版本。

  • 如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,直接返回该版本的数据;
  • 如果被访问版本的trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,直接返回该版本的数据;
  • 如果被访问版本的trx_id,大于或等于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,此时该版本不可以被当前事务访问,需要通过隐藏的回滚指针从undo log中读取历史版本;
  • 如果被访问版本的trx_id,在readview的min_trx_id和max_trx_id之间,则需要判断trx_id值是否在m_ids列表中?
    (1)如果在:说明readview创建时,创建该版本数据的事务还未提交,因此需要通过回滚指针读取历史版本并返回。
    (2)如果不在:说明readview创建时,创建该版本数据的事务已经提交,所以直接返回该版本的数据;

InnoDB如何解决各种隔离问题

第一、InnoDB是如何解决脏读问题的?

如果是读已提交,那么事务每一个语句执行前都会重新计算出新的视图,也就解决了脏读问题。

第二、InnoDB是如何解决不可重复读问题的?

如果是可重复读,那么事务开启时创建一次访问视图。同一个事务中后续所有的查询共用一个ReadView,由此便解决了不可重复读的问题。

第三、InnoDB是如何解决幻读问题的?

在MVCC并发控制中,读操作可以分成两类:
快照读(snapshot read):读取的是记录的可见版本(有可能是历史版本),不用加锁(共享读锁s锁也不加,所以不会阻塞其他事务的写)
当前读(currentread):读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录

快照读、锁定读:了解这两种读取方式的发生时机以及如何实现的?

快照读是指通过MVCC实现的非阻塞读,常见的快照读操作如下:

select xxx from xxx

当前读也叫加锁读,每次读取数据都是读取数据的最新版本,并且会对其进行加锁。常见的当前读操作如下

  • select xxx from xxx lock in share mode (共享锁/读锁)
  • select xxx from xxx for update (排它锁/写锁)
  • update 、delete、insert

为什么要区分这两种读操作呢?因为MVCC并不能解决幻读的问题。即使是在可重复读级别,通过当前读依然会出现幻读问题。

此问题最终是通过间隙锁(next-key lock)来解决的。

详细可参考:
https://blog.csdn.net/weixin_42612223/article/details/134623841

InnoDB是如何解决事务的持久性问题

image

redo log buffer (内存中)是由首尾相连的四个文件组成的,它们分别是:ib_logfile_1、ib_logfile_2、ib_logfile_3、ib_logfile_4。

  • write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
  • checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
  • write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。
  • 如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
  • 有了 redo log,当数据库发生宕机重启后,可通过 redo log将未落盘的数据(check point之后的数据)恢复,保证已经提交的事务记录不会丢失,这种能力称为crash-safe。

Redo log是什么? 通过它解决了什么问题?

redo log是InnoDB存储引擎为了解决事务持久性而引入的WAL技术。借助redo log,InnoDB存储引擎将事务的commit提交简化为一次内存操作与一次磁盘写入操作。如果磁盘页中的数据发生了丢失,也就是在崩溃恢复过程中,存储引擎会通过重做redo log中的操作来进行数据恢复。

binlog又是什么?他是干什么用的? 了解主从同步原理。

binlog是Mysql server层为了解决主从数据同步而引入的一套日志系统。binlog中记录的是一个数据行发生了什么操作。

image

Redo Log与binlog的两阶段提交

  • prepare阶段,先写入rede log(状态为准备中)
  • 写入binlog(状态为已提交)—- TX_ID
  • commit阶段,写入redo log(状态为已提交)

而两阶段提交就是让这两个状态保持逻辑上的一致。redolog 用于恢复主机故障时的未更新的物理数据,binlog 用于备份操作。两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。

为什么需要两阶段提交呢?

如果不用两阶段提交的话,可能会出现这样情况
先写 redo log,crash 后 bin log 备份恢复时少了一次更新,与当前数据不一致。
先写 bin log,crash 后,由于 redo log 没写入,事务无效,所以后续 bin log 备份恢复时,数据不一致。
两阶段提交就是为了保证 redo log 和 binlog 数据的安全一致性。只有在这两个日志文件逻辑上高度一致了才能放心的使用。
在恢复数据时,redolog 状态为 commit 则说明 binlog 也成功,直接恢复数据;如果 redolog 是 prepare,则需要查询对应的 binlog事务是否成功,决定是回滚还是执行。

InnoDB的几个关键特性

insert buffer、double write、自适应hash索引、异步写等

参考:
https://blog.csdn.net/hahazz233/article/details/125372412

数据库中涉及到锁的一些经常考察的知识点

数据库锁

悲观锁 VS 乐观锁

首先悲观锁(Pessimistic Lock)、乐观锁并不是两种锁,而是两种思想,两种用于实现并发控制的思想。

其中,悲观锁指的是对数据被外界修改持悲观态度,认为数据大概率会被他人修改,所以在我准备修改数据时,我会对该数据加锁以避免其他人对该数据进行并发访问。

而乐观锁指的是对外部修改数据持乐观态度,认为数据不会修改,所以我会直接对数据进行修改,在修改的以后再进行冲突的检查。如果修改失败了,我再决定如何去做。

悲观锁适合写多读少的场景,而乐观锁适合读多写少的场景。

悲观锁一般通过数据库锁来实现,而乐观锁一般是通过CAS来实现。

意向锁

意向锁是什么? 为什么需要意向锁?

意向锁是实现多粒度锁的一种方式,是表锁,分为意向排他锁、意向共享锁;

意向锁之间是兼容的;意向锁与表级别的共享锁、拍他锁是可能互斥的;

意向锁与行级的互斥锁、共享锁是兼容的;

实现意向锁的目的有两个:第一,让多粒度锁共存;第二,加快是否可以加锁的判定效率。

考虑一种场景,事务A尝试修改一条数据,此时事务B需要加表锁。 如果没有意向锁,数据库系统需要怎么做? - 系统需要扫描对所有的行加锁。

意向锁也不会和数据行共享锁S、排它锁X发生冲突。

那这玩意干啥的?

update new_table set user_id = 911 where id = 1;
假设我们执行这么一条语句,innodb除了在id=1的这条记录上增加了行级X锁之前,还对所在表添加了一个意向排它锁。

这个时候如果我们有针对 new_table 的表级锁操作,如:alter table、drop table、lock table 的操作时,会先检查对应表是否存在意向排它锁,若存在则等待锁释放。

意向共享锁 意向互斥锁
表级共享锁 兼容 互斥
表级互斥锁 互斥 互斥
意向共享锁 意向互斥锁
行级共享锁 兼容 兼容
行级互斥锁 兼容 兼容
意向共享锁 意向互斥锁
意向共享锁 兼容 兼容
意向互斥锁 兼容 兼容

九、行级锁定的优点:
1.当在许多线程中访问不同的行时只存在少量锁定冲突。
2.回滚时只有少量的更改
3.可以长时间锁定单一的行。
十、行级锁定的缺点:
比页级或表级锁定占用更多的内存。当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁。 如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多。 用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序,因为其锁成本小于行级锁定。

next-key lock

Record lock、gap lock、next-key lock

  • record lock, 行锁, 锁直接加在索引记录上面,锁住的是key。
  • gap lock, 间隙锁,用来解决幻读问题
  • next-key lock, gap lock + reocrd lock

关于next-key lock的两个原则、两个优化、一个bug:

  • 原则1: 加锁的基本单位是next-key lock, 前开后闭区间,(A, B]
  • 原则2: 查找过程中遇到的对象才会加锁(延迟加锁)
  • 优化1: 索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁
  • 优化2: 索引上的等值查询,向右遍历时且最后一个值不满足等值条件时,next-key lock退化为gap lock
  • bug: 唯一索引的范围查找会访问到不满足条件的第一个值为止。

参考:https://blog.csdn.net/zwx900102/article/details/106544634?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

———-User——-

application write(fd,buf.len)

———-Kernel——-

File Validate file descriptor.
Sockets Copy/append buf to socket buffer.
TCP Create TCP segment according to TCP state, Compute checksum.
IP Add IP header,perform IP routing. Compute checksum.
Ethernet Add Ethernet header,perform ARP.
Driver Tell NIC to send the packet.

———-Device——-
NIC Fetch the packet from host memory and send it. Interrupt the host when send is done.

缓冲区被塞满

如图所示,物理介质上的数据帧到达后首先由NIC(网络适配器)读取,写入设备内部缓冲区Ring Buffer中,再由中断处理程序触发Softirq从中消费,Ring Buffer的大小因网卡设备而异。当网络数据包到达(生产)的速率快于内核处理(消费)的速率时,Ring Buffer很快会被填满,新来的数据包将被丢弃;

报文mac地址丢包

一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据,如果报文的目的mac地址不是对端的接口的mac地址,一般都会丢包,一般这种情况很有可能是源端设置静态arp表项或者动态学习的arp表项没有及时更新,但目的端mac地址已发生变化(换了网卡),没有更新通知到源端(比如更新报文被丢失,中间交换机异常等情况);

长链接 VS 短链接

长连接:

长连接多用于操作频繁,点对点的通讯(尤其是需要下行消息的场景,例如即时通信),而且连接数不能太多的情况。

短连接:

web网站的http服务一般都用短连接。因为长连接对于服务器来说要耗费一定的资源。

像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接更省一些资源。试想如果都用长连接,而且同时用成千上万的用户,每个用户都占有一个连接的话,可想而知服务器的压力有多大。所以并发量大,但是每个用户又不需频繁操作的情况下需要短连接。

TCP与UDP的区别

流模式 VS 数据报模式、连接 VS 无连接、可靠性

TCP与UDP应用

TCP:对效率要求低,对准确性要求较高 (如文件传输、重要状态的更新等)

eg. STMP, TELNET, HTTP, FTP

UDP:对效率要求高,对准确性要求较低 (如视频传输、实时通信等)。

eg. DNS,TFTP,RIP,DHCP,SNMP