想法:

一个脚本管理工具,类似 alias 的工具

  1. 使用 sc add xxx 来添加需要运行的脚本,可以选择执行脚本的程序,默认是 sh
  2. 使用 sc run 来运行某个脚本

重点看 io uring, ebpf

kafka docker compose 示例

version: '3'

name: kafka-group

services:
  zookeeper-test:
    image: zookeeper
    ports:
      - "2181:2181"
    volumes:
      - zookeeper_vol:/data
      - zookeeper_vol:/datalog
      - zookeeper_vol:/logs
    container_name: zookeeper-test

  kafka-test:
    image: wurstmeister/kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_HOST_NAME: "localhost"
      KAFKA_ZOOKEEPER_CONNECT: "zookeeper-test:2181"
      KAFKA_LOG_DIRS: "/kafka/logs"
    volumes:
      - kafka_vol:/kafka
    depends_on:
      - zookeeper-test
    container_name: kafka-test

volumes:
  zookeeper_vol: {}
  kafka_vol: {}

Docker 安装 MySQL

mkdir mysql-data
cd mysql-data
mkdir log
mkdir data
mkdir conf

docker run --name mysql-dev --rm -e MYSQL_ROOT_PASSWORD=qwe -d mysql
docker cp mysql-dev:/etc/mysql ./conf/
docker stop mysql-dev

docker run -p 3306:3306 --name mysql-dev --privileged=true \
-v ./log:/var/log/mysql \
-v ./data:/var/lib/mysql \
-v ./conf/mysql:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=qwe -d mysql:latest

Docker 安装 ClickHouse

# 准备环境
mkdir clickhouse-data
cd clickhouse-data
mkdir data
mkdir conf
mkdir log

# 复制配置文件

sudo docker run -d --rm --name clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server
sudo docker cp clickhouse-server:/etc/clickhouse-server/config.xml ./conf/config.xml
sudo docker cp clickhouse-server:/etc/clickhouse-server/users.xml ./conf/users.xml
sudo docker stop clickhouse-server

# 启动镜像
sudo docker run -d --name=clickhouse-server \
-p 8123:8123 -p 9090:9000 \
--ulimit nofile=262144:262144 \
-v ./data:/var/lib/clickhouse:rw \
-v ./conf/config.xml:/etc/clickhouse-server/config.xml \
-v ./conf/users.xml:/etc/clickhouse-server/users.xml \
-v ./log:/var/log/clickhouse-server:rw \
clickhouse/clickhouse-server

MySQL 优化策略

刷脏页: 脏页表示还没有写入磁盘的数据页. 什么情况下会触发?

  1. redo log 写满了
  2. 内存不足, 需要淘汰一些数据页, 如果淘汰的是脏页就需要 flush
  3. 空闲时
  4. 关闭 server 时

1 和 2 会影响我们执行 SQL 的速度, 1 我们需要避免, 因为通常执行更新操作都是写 redo log

优化方案:

  1. 正确的设置 innodb_io_capacity
  2. 关注脏页比例 select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';select @a/@b;
  3. 正确设置 innodb_flush_neighbors 来控制刷脏页时要不要一直刷临近的脏页
  4. 正确设置 redo log 大小

image

事务到底是隔离还是不隔离?

假设在所有事务开始时 k = 1, 那么

  1. 事务A 读取到 k = 1
  2. 事务B 读取到 k = 3
  3. 事务C 更新 k = 2

为什么会这样?

1, 事务C 更新到 2 没问题
2. 事务B 的更新操作为什么会读取到 2?
- 因为更新数据都是先读后写的,读这个操作是 当前读,即读取最新值
- 如果 select 加锁也会是当前读
3. 事务A 因为可重复读所以还是 1

也可以这样解释:

  1. 假设 A 的事务 id 为 99, B 为 100, C 为 101
  2. 事务A 的视图数组为 , B 为 , C 为
  3. id = 1 的这一行原事务 id 为 90
  4. 当 C 更新时, 变成了 101
  5. 当 B 更新时, 变成了 100
  6. 当 B 读取时, 100 在它的视图数组中就可以读取
  7. 当 A 读取时, 100 不再它的视图数组中, 就必须回滚: 100 -> 101 -> 90, 最后得到 1

image

如果事务C 没有在更新完第一时间提交会怎么样?

如果事务C 不提交则表明它还持有写锁, 而事务B 的 update 语句要获取读锁, 读写锁互斥, 因此被锁住了.

全局锁:

对整个数据库实例加读锁 Flush tables with read lock, 使整个数据库处于只读状态, 其他更新、修改等语句会阻塞

主要应用场景是全库备份, 但不推荐这样使用(除非引擎不支持事务), 备份可以用 mysql dump 使用参数 -single-transcation, 导数据之前开启一个数据, 使用视图来读取数据

表锁:

  1. 第一种语法 lock tables ... read/write 可以用 unlock tables 主动释放, 如果使用了 lock tables 就只能操作指定的 table 且其他线程读写 lock 的表会被阻塞
  2. 另一种是 MDL, 在访问一个表的时候会自动加上. 用于保证读写的正确性
    • 读锁是对一个表进行增删改查时加
    • 写锁是对表结构做变更时加
    • 读锁之前不互斥, 所以可以有多个线程对同一张表增删改查
    • 读写锁、写锁之间是互斥的, 用于保证变更表结构操作的安全性

image

三亚的太阳

覆盖索引

这是一条建表 SQL 以及一条初始化 SQL,如果执行 select * from T where k between 3 and 5 需要执行几次树的搜索操作?

create table T (
ID int primary key,
k int NOT NULL DEFAULT 0, 
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;

insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');

image

  1. 在 k 索引树上找到 k=3 的记录,取得 ID = 300
  2. 再到 ID 索引树查到 ID=300 对应的 R3
  3. 在 k 索引树取下一个值 k=5,取得 ID=500
  4. 再回到 ID 索引树查到 ID=500 对应的 R4
  5. 在 k 索引树取下一个值 k=6,不满足条件,循环结束

在这个过程中读取了 k 索引树的 3 条数据以及回表 2 次,那么有没有可能避免回表操作?

如果执行 select ID from T where k between 3 and 5 这表明只需要查询 id 的值,而 id 的值已经在 k 索引树上了,可以直接提供结果不需要回表. 也就是说,在这个查询中,索引 k 已经覆盖了我们的查询需求,这就是 覆盖索引

最左前缀原则

联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符

索引项是按照索引定义里面出现的字段顺序排序的,例如我们要找以张为姓的所有人可以用where name like '张%' 就可以命中索引,但如果使用'%张' 是不会命中索引的

image

索引下推

最左前缀可以用于在索引中定位记录,但如果有不符合最左前缀的部分会怎么样?

有一个需求是要查询名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的:

select * from tuser where name like '张%' and age=10 and ismale=1;

这个语句在搜索索引树的时候,只能用 “张”,找到第一个满足条件的记录 ID3,然后索引就会失效.

在 MySQL5.6 以前时只能从 ID3 开始一个个回表, 到主键索引树上找出数据行然后对比数据

但是在 5.6 以后可以在索引遍历过程中对索引中包含的字段先做判断,直接过滤到不满足条件的记录,减少回表次数.

即以前是不会判断 age 这个字段的,但因为我们的索引是包含 age 字段的,所以现在可以利用索引下推这个功能来有优化查询速度.

InnoDB 的索引模型:

在 InnoDB 中,每一个索引都对应一颗 B+ 树,有下面的建表 SQL,id 与 k 都是索引列:

create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k) 
)engine=InnoDB;

image

可以看出索引分为 主键索引 以及 非主键索引

  1. 主键索引(聚簇索引的叶子节点存放了整行的数据,
  2. 非主建索引(二级索引)的叶子节点存放的是主键的值

基于主键索引和普通索引的查询有什么区别?

  • 如果是 select * from t where id = 1 使用主键索引则只需要查找 ID 索引树
  • 如果是 select * from t where k = 6 则需要先查找 k 索引树然后在到 ID 索引树中在查找. 这个过程就是 回表

为什么推荐使用自增主键?

B+树为了保证索引的有序性,在插入以及删除值的时候进行必要的维护.以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。

而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。

而使用一个递增的主键就可以保证每次插入都是追加,写数据性能就会提高.

事务之隔离性

当数据库上有多个事务同时执行的时候,就可能出现脏读、不可重复读、幻读等问题,这就需要靠隔离级别来解决.

隔离级别包括:

  1. 读未提交: 一个事务还没提交时,它的变更就能被其他事务看到
    • 直接返回记录上的最新值
  2. 读提交: 一个事务提交后,它做的变更才会被其他事务看到
    • 视图在每个 SQL 语句开始执行的时候创建的
  3. 可重复读: 一个事务执行过程中看到的数据,总是跟这个事务在启
    动是看到的数据是一致的,当然这个事务的变更也对其他事务不可见
    • 在事务启动时创建视图, 整个事务都使用这个视图来访问数据
  4. 串行: 对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
    • 直接用加锁的方式来避免并行访问

MySQL 默认隔离级别是 可重复读,Oracle 是 读提交.

怎么实现事务隔离?

  1. 每条 SQL 执行后都会记录一条对应的回滚操作 undo log
  2. 不同时刻启动的事务都有不同的 视图

这就会使得一条记录在系统中的同一时刻会有不同的版本,如果视图 A 要得到 1,就必须依次执行下面的所有回滚操作得到

image

书籍收藏:

  1. DDIA(设计数据密集型应用程序)
  2. Rust Atomics and Locks
  3. 深入架构原理与实践

一条更新 SQL 的执行流程:

  1. 总体流程和查询的 SQL 执行流程类似, 但更新流程涉及到两个重要的日志模块: redo log(重做日志), binlog(归档日志)
  2. 关键点是先写日志, 在写磁盘 —— InnoDB 会先把记录写道 redo log 里面并更新内存, 这个时候更新操作就算完成了, 后续会在适当的时候写入到磁盘中(比如系统比较空闲).

重要的日志模块: redo log

InnoDB 的 redo log 是固定大小的, 比如可以配置为一组 4 个文件, 每个文件的大小是 1GB, 即总容量是 4GB. 从头开始写, 写倒末尾就就又回到开头循环写:

image

  1. write pos 是当前记录的位置, 一边写一边后移, 写到 3 号文件末尾就会到 0 号文件重新开始
  2. check point 是当前要擦除的位置, 同样是循环的, 擦除记录前要把记录更新到数据文件
  3. write poscheck point 之间的空间就是可以用来记录新的操作, 如果这两个值相等就表示不能继续写了, 得先写入一些数据到磁盘(check point 擦除).

有了 redo log, InnoDB 可以保证即使数据库发生异常重启, 之前提交的记录都不会丢失 —— crach-safe

重要的日志模块: binlog

binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。

  1. binlog server 自带的而 redo log 是 InnoDB 独有的
  2. redo log 是物理日志, 记录了在某个数据页上做了什么修改, binglog 是逻辑日志, 记录的是语句的原始逻辑, 比如"给 ID = 2 的这一行的 age 字段加 1"
  3. redo log 是循环写, binglog 是追加的, 在文件到达一定大小会切换到下一个

所以执行器和引擎执行更新 SQL 的流程:

  1. 执行器先找引擎取 ID = 2 的这一行, 假如 ID 为主键, 引擎就直接用树搜索找到这一行, 如果这一行所在的数据页本来就在内存中, 就直接返回, 否则就先从磁盘读入内存然后返回
  2. 执行器拿到引擎给的数据, 执行更新操作, 然后在调用引擎写入数据
  3. 引擎将这行数据更新到内存中, 同时将更新操作记录到 redo log 中, 此时 redo log 处于 prepare 状态. 然后告知执行器执行完成了, 随时可以提交事务
  4. 执行器生成这个操作的 binlog, 并把 binlog 写入磁盘
  5. 执行器调用引擎的提交事务接口, 引擎把刚刚写入的 redo log 改为 commit 状态, 更新完成

image

为什么需要两阶段提交?

  1. 如果在 prepare 阶段崩溃, 重启恢复后发现没有 commit 会回滚. 备份恢复, 发现没有这条记录的 binlog. 数据一致
  2. 如果在 commit 时崩溃, 重启恢复时满足 prepare 和 binlog 完整会自动 commit . 备份恢复时有binlog 数据一致

一条查询 SQL 的执行过程:

  1. client 与连接器建立连接,连接器确认 client 认证信息以及权限信息
  2. 分析器进行词法分析(select, where, update, group by...), 然后进行语法分析
  3. 优化器选择索引生成执行计划
  4. 执行器调用存储引擎的读写接口进行数据查询

image