mysql整理笔记
mysql安装:
安装:
wget https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.15-1.el7.aarch64.rpm-bundle.tar
sudo rpm -Uvh mysql80-community-release-el7-2.noarch.rpm
yum install -y mysql-community-server # 安装mysql服务端
sudo service mysqld start 相当于centos7的systemctl start mysqld.service # restart
service mysqld status 查看是否启动成功或失败原因,systemctl status mysql.service,启动失败可以看error log有输出
ubuntu下:
sudo apt-get install mysql-server
sudo apt install mysql-client
sudo apt install libmysqlclient-dev
配置文件路径:
/etc/my.cnf
/etc/mysql/my.cnf
/var/lib/mysql/my.cnf
启动mysql服务与停止mysql服务命令:
sudo service mysqld start service mysqld restart (rpm安装)
登陆与退出命令:
mysql -h 服务器IP -P 端口号 -u 用户名 -p 密码 --prompt 命令提示符 --delimiter 指定分隔符
mysql -h 127.0.0.1 -P 3306 -u root -p
常见安装以及操作问题:
1.关于root用户默认不能远程登录的问题:
默认本地不能通过内网或者127.0.0.1都可以登录的
skip-name-resolve
配置环境变量:
MYSQL_ROOT_HOST=%
2.shell无法输入中文:
容器启动需要-e LANG=C.UTF-8
docker run -d -it -e MYSQL_ROOT_PASSWORD=123 --name mysql2 -e LANG=C.UTF-8 -p 3306:3306 mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
3.NFS上mysql启动不了的问题:
dmesg提示lock server等待
mount -o nolock才行
用户管理:
修改密码:
注意:
mysql5.7.6版本后 废弃user表中 password字段 和 password()方法,所以旧方法重置密码对mysql8.0版本是行不通的
方法一(配置):
vim /etc/my.cnf
在【mysqld】模块添加:skip-grant-tables
service mysqld restart # 重启配置,然后免密码登陆,use mysql,清空密码,更新配置重启,登陆,设置新密码:
ALTER USER 'root'@'%' IDENTIFIED BY 'root123';
方法二(第一次登陆):
grep 'temporary password' /var/log/mysqld.log 找到临时密码
登陆
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root123';
方式三(5.x版本):
update user set Host="%" where User="root"; # 可以改Host
方式四(8.x版本):
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'corona@2020@wuhan';
grant all privileges on *.* to 'root'@'localhost' with grant option; 这个方法只能修改权限,不能改Host
FLUSH PRIVILEGES;
忘记密码:
方式一:
vi /etc/my.cnf 在[mysqld]的段中加上一句:skip-grant-tables 保存并且退出vi。
service mysqld restart
mysql -uroot
USE mysql; UPDATE user SET Password = password ( 'new-password' ) WHERE User = 'root' ;
update mysql.user set authentication_string=password('123456a') where user='root';
还原my.cnf配置
service mysqld restart
方式二:
直接停了再额外参数启动,mysqld --skip-grant-tables &
指定账号语法:
1.语法是'user_name'@'host_name'。
2.只指定username时,等价于'user_name'@'%'。
3.如果username和hostname未加引号时合法,可以不加引号,不合法如用户名包含-,空格,hostname包含. % 。
4.引号可以使用单引号、双引号、反引号。
5.当前账号可以用 CURRENT_USER 或 CURRENT_USER()。
6.host字段可以使用%和_(匹配任意一个字符),类似like中的用法。
注:172.31.% 在Mysql中不会匹配172.31.example.com这类以数字和点开头的域名。
7. IPv4可以使用netmask,如CREATE USER 'david'@'198.51.100.0/255.255.255.0'即198.51.100开头的0-255范围的ip。
8.客户端的host name或ip会先被系统的dns解析后返回给mysql,用来匹配对应账号的host字段,所以本地dns返回的格式很重要如198.051.100就不规范。
关于mysql数据库:
user表每个账号有一行数据,User和Host列,以及账号的权限
其他表存放账号对数据库和表的权限,也有User和Host列,对应着user表的值
User字段是大小写区分的,Host字段不区分大小写
修改远程访问ip:
方法一:
use mysql
select host,user,authentication_string,plugin from user;
update user set host = "%" where user = "root"; # 更新host的时候不会保留原来的权限
FLUSH PRIVILEGES;
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'WITH GRANT OPTION;
方法二:
RENAME USER 'test'@'172.%' TO 'test'@'%'; 保留原来的权限
查看连接ip信息:
select SUBSTRING_INDEX(host,':',1) as ip , count(*) from information_schema.processlist group by ip;
创建用户:
示例:
CREATE USER 'czl'@'172.31.%' IDENTIFIED BY 'Czl1234.';
CREATE USER 'test'@'%' IDENTIFIED WITH mysql_native_password BY 'Czl1234.';
grant all privileges on *.* to 'czl'@'172.31.%'; # 需要grant权限,查user表可看,注意name和domain必须一致
# 8.0之前版本可以通过这个命令创建新的用户+domain
如何匹配多个网段:
1.update mysql.user set Host="172.%" where User='czl'; # domain是表的一个columns,只能update,不能grant更新
2.再创建一个不同domain相同name的用户:
mysql会根据domain和username来匹配用户,domain和username是联合主键,Duplicate entry '172.%-test' for key 'PRIMARY'(旧版本也这样)
mysql会优先匹配出domain匹配度高的那一条数据,如172.%比%优先,然后使用那条数据的密码和权限配置
旧版本也是根据username+domain来做联合主键,只是grant能否创建新账号的区别
旧版本创建方式:
CREATE USER 'grafana'@'%' IDENTIFIED WITH mysql_native_password BY 'Grafana1234.';(这个不变)
或grant all privileges on *.* to 'czl'@'172.31.%';
修改权限:
grant select on *.* to 'czl'@'172.31.%'; # 如果已有该权限,不变,如果没有就更新。范围变化也会更新(范围存放其他表)
# 需要注意username和domain要一致
删除用户:
方法一:
delete from user where user="czl";
FLUSH PRIVILEGES;
方法二:
Drop user 'test'@'%'; # 'test'等价于'test'@'%'
撤销权限:
revoke super on *.* from 'czl'@localhost; # from
设置只读:
show global variables like "%read_only%";
set global read_only=1; # 对拥有super权限的账号是不生效的
常见规范:
1. 在数据库系统中,SQL语句不区分大小写(建议用大写) 。但字符串常量区分大小写。建议命令大写,表名库名小写。
2. SQL语句可单行或多行书写,以“;”结尾。关键词不能跨多行或简写。
3. 用空格和缩进来提高语句的可读性。子句通常位于独立行,便于编辑,提高可读性。
4. 注释:单行注释:-- 多行注释:/*......*/(js)
5. 命名和内置关键词冲突时,使用``引号。
6. DML、DDL、DCL区别
DML(data manipulation language):用来对数据库里的数据进行操作的语言
DDL(data definition language):主要是用在定义或改变表(TABLE)的结构,数据类型,表之间的链接和约束等
初始化工作上,他们大多在建立表时使用
DCL(Data Control Language):是数据库控制功能。是用来设置或更改数据库用户或角色权限的语句
7. 三大范式:
1NF:字段不可分;
2NF:有主键,非主键字段依赖主键;
3NF:非主键字段不能相互依赖;
数据库反三范式
反范式化指的是通过增加冗余或重复的数据来提高数据库的读性能
使用场景
当需要查询“订单表”所有数据并且只需要“用户表”的name字段时, 没有冗余字段 就需要去join 连接用户表,假设表中数据量非常的大, 那么会这次连接查询就会非常大的消耗系统的性能. 这时候冗余的字段就可以派上用场了, 有冗余字段我们查一张表就可以了.
数据类型:
1.数字
TINYINT --小整数,特别的: MySQL中无布尔值,使用tinyint(1)构造。
SMALLINT
MEDIUMINT
INT\INTEGER --整数类型中的m仅用于显示,对存储范围无限制。int(5),当插入数据2时,select 时数据显示为: 00002。8.0之前会存在主键回溯问题(8.0后持久化主键,delete不影响)达到上限后会报主键重复。
BIGINT -- 推荐主键类型。
FLOAT(m,d) --单精度浮点数(非准确小数值),m是数字总个数,d是小数点后个数。
DOUBLE(m,d) --数值越大,越不准确
DECIMAL(m,d) --对于精确数值计算时需要用此类型,参数m<65 是总个数,d<30且 d<m 是小数位。m-d即为数字整数位数大小,变长。金融行业推荐decimal转bigint类型,性能更好,存储更紧凑
2.日期/时间
DATE YYYY-MM-DD
TIME HH-MM-SS
YEAR
DATETIME YYYY-MM-DD HH-MM-SS,Datetime(n)n表示毫秒的精度,固定8个字节。推荐,不存在时区转换问题
TIMESTAMP YYYYMMDD HHMMSS存储1971年到现在的毫秒数(7字节),最大的优点是带有时区属性。需要从毫秒数转换,存在性能问题
3.字符串
CHAR --定长,0-255字符长度,规定字符长度n,拿取效率高,即使数据小于m长度,也会占用m长度,查询性能更好。存储按照指定长度,检索时去掉尾随空格。
VARCHAR # 变长,只取需要长度。更新可能会导致碎片问题(原位置不满足长度)。注意在utf8mb4字符格式下,固定用4个字节。
TINYBLOB
TINYTEXT
BLOB # 二进制
TEXT # 长文本
MEDIUMBLOG
MEDIUMTEXT
LONGBLOB
LONGTEXT
JSON 5.7出现,8.0解决存储性能问题。提供很多读取操作,比较方便。
4.枚举类型
enum --size ENUM('x-small', 'small', 'medium', 'large', 'x-large')
5.集合类型
set --SET('a', 'b', 'c', 'd')
JSON:
概述:
与text的区别:多了在数据库操作原生字典的方法
创建:
JSON NOT NULL,
插入:
INSERT INTO t_json(id,sname,info) VALUES( 1, 'name1', JSON_ARRAY(1, "abc", NULL, TRUE, CURTIME()));
INSERT INTO t_json(id,sname,info) VALUES( 2, 'name2', JSON_OBJECT("age", 20, "time", now()));
INSERT INTO t_json(id,sname,info) VALUES( 3, 'name3', '{"age":20, "time":"2018-07-14 10:52:00"}');
SELECT JSON_MERGE_PRESERVE('["a", 1]', '{"key": "value"}');
SELECT JSON_QUOTE('[1,2,3]');
操作:
判断类型:
SELECT JSON_TYPE('["a", "b", 1]');
SELECT JSON_TYPE('"hello"');
是否有效:
SELECT JSON_VALID('null'), JSON_VALID('Null'), JSON_VALID('NULL'); # 敏感,而sql内置对于大小写不敏感
SELECT CAST('null' AS JSON);
查找指定数据:
JSON_EXTRACT(json_doc, path[, path] ...)从json文档里抽取数据。如果有参数有NULL或path不存在,则返回NULL。如果抽取出多个path,则返回的数据封闭在一个json array里。
SELECT JSON_EXTRACT('{"id": 14, "name": "Aztalan"}', '$.name');
SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.*');
SELECT JSON_EXTRACT('{"a": 1, "b": 2, "c": [3, 4, 5]}', '$.c[*]'); # 获取key为c的所有内容
SELECT JSON_EXTRACT('{"a": {"b": 1}, "c": {"b": 2}}', '$**.b'); # $**获取所有的key的b对应value
SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[1 to 3]'); # list操作
SELECT JSON_EXTRACT('[1, 2, 3, 4, 5]', '$[last-3 to last-1]');
SELECT JSON_REPLACE('"Sakila"', '$[last]', 10); # 注意不是数组时替换整个
设置:
SELECT JSON_SET('"x"', '$[0]', 'a','$[1]', 'b'); # 已存在会覆盖
新增:
SELECT JSON_INSERT(@j, '$[1].b[0]', 1, '$[2][2]', 2);
替换:
SELECT JSON_REPLACE(@j, '$[1].b[0]', 1, '$[2][2]', 2);
删除:
SELECT JSON_REMOVE(@j, '$[2]', '$[1].b[1]', '$[1].b[1]');
指定数据是否存在
JSON_CONTAINS(json_doc, val[, path])# 查询json文档是否在指定path包含指定的数据,包含则返回1,否则返回0。如果有参数为NULL或path不存在,则返回NULL。
SELECT JSON_CONTAINS(@j, '4', '$.c.d');
指定路径是否存在
JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)# 查询是否存在指定路径,存在则返回1,否则返回0。如果有参数为NULL,则返回NULL。
set @j = '{"a": 1, "b": 2, "c": {"d": 4}}';
SELECT JSON_CONTAINS_PATH(@j, 'one', '$.a', '$.e'); -- 1
SELECT JSON_CONTAINS_PATH(@j, 'all', '$.a', '$.c.d'); -- 1
查找所有指定键值
JSON_KEYS(json_doc[, path]) 获取json文档在指定路径下的所有键值,返回一个json array。如果有参数为NULL或path不存在,则返回NULL。
SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}'); -- ["a", "b"]
SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}', '$.b'); -- ["c"]
查找指定值(key or value)的位置
JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])
# 查询包含指定字符串的paths,并作为一个json array返回。如果有参数为NUL或path不存在,则返回NULL。
# one_or_all:"one"表示查询到一个即返回;"all"表示查询所有。
# search_str:要查询的字符串。 可以用LIKE里的'%'或‘_’匹配。
示例:
SET @j3 = '["abc", [{"k": "10"}, "def"], {"x":"abc"}, {"y":"bcd"}]';
SELECT JSON_SEARCH(@j3, 'one', 'abc'); -- "$[0]"
SELECT JSON_SEARCH(@j3, 'all', 'abc'); -- ["$[0]", "$[2].x"]
SELECT JSON_SEARCH(@j3, 'all', 'abc', NULL, '$[2]'); -- "$[2].x"
SELECT JSON_SEARCH(@j3, 'all', '10'); -- "$[1][0].k"
SELECT JSON_SEARCH(@j3, 'all', '%b%'); -- ["$[0]", "$[2].x", "$[3].y"]
SELECT JSON_SEARCH(@j3, 'all', '%b%', NULL, '$[2]'); -- "$[2].x"
字符集:
char和varchar的影响:
8.0之后默认为utf8mb4(一个字符最多4个字节),之前的utf8,会存在插入表情字符异常问题(utf8范围不够)。
对于单一字符比如a,varchar存储2个字节(一个用来存长度),char存储1个字节
对于不同字符,比如a和"我",char(4)分别存储1+3个字节和3+3个字节。
结论:对于字符类型不固定的字段,用varchar和char都会产生硬盘碎片化。文件都是按照字节byte来读取的。
设置:
character-set-server=utf8mb4
修改表字符集:
alter table xxx convert to character utf8mb4
DDL操作:
1.创建数据库
create database [if not exists] db_name [character set xxx]
CREATE DATABASE IF NOT EXISTS my_db default charset utf8 COLLATE utf8_general_ci;
2.查看数据库
show databases;查看所有数据库
show create database db_name; 查看数据库的创建方式
自带数据库:
information_schema: 信息数据库,记录其他数据库的信息
sys: dba运维需要,一些慢查询sql的记录
performance_schema: 性能
3.修改数据库
alter database db_name [character set xxx]
4.删除数据库
drop database [if exists] db_name;
5.使用数据库
切换数据库 use db_name; -- 注意:进入到某个数据库后没办法再退回之前状态,但可以通过use进行切换
查看当前使用的数据库 select database();
6.创建表
create table tab_name(
field1 type[完整性约束条件],
field2 type,
...
fieldn type
KEY `id` (`id`) # 圆括号内的id是字段名称,圆括号左侧外面的id是索引名称。
)[character set xxx][ENGINE=innodb\mysaim]; # innodb支持事务、行锁、表锁;mysaim支持表锁
索引约束:
primary key (非空且唯一) :能够唯一区分出当前记录的字段称为主键!
unique
not null
auto_increment 主键字段必须是数字类型。
外键约束:
foreign key */
其他方式:
create table aa(select * from bb) --把数据都复制过来,而外键那些不复制
复制表结构:
create table a like b
部分克隆:
CREATE TABLE A AS SELECT x,x,x,xx FROM B LIMIT 0
7.查看表信息:
desc tab_name 查看表结构
show columns from tab_name 查看表结构 --效果一样
show tables 查看当前数据库中的所有的表
show create table tab_name 查看当前数据库表建表语句
8.修改表结构:
(1)增加列(字段)
alter table tab_name add [column] 列名 类型[完整性约束条件][first|after 字段名];
alter table users2
add addr varchar(20),
add age int first,
add birth varchar(20) after name;
(2)修改一列类型
alter table tab_name modify 列名 新类型 [完整性约束条件][first|after 字段名];
(3)修改列名(可修改类型)
alter table tab_name change [column] 列名 新列名 类型 [完整性约束条件][first|after 字段名];
(4)删除一列
alter table tab_name drop [column] 列名;
(5)修改表名
rename table 表名 to 新表名;
(6)修该表所用的字符集
alter table student character set utf8;
(7)修改索引主键
先删除再添加
alter table featuredatadb.r_cash_loan_old_user_model_result drop primary key;
alter table featuredatadb.r_cash_loan_old_user_model_result add primary key (`uid`,`cash_loan_id`,`model_version`);
9.删除表
drop table tab_name;
10.修改默认值:ALTER TABLE testalter_tbl ALTER i SET DEFAULT 1000;
删除默认值:ALTER TABLE testalter_tbl ALTER i DROP DEFAULT;
DML操作:
INSERT:
语法:
insert [into] tab_name (field1,filed2,.......) values (value1,value2,.......);
插入多条数据(不带字段时默认全部字段):
insert into employee_new values (4,'alvin1','1993-04-20',3000),(5,'alvin2','1995-05-12',5000);
set插入:
insert [into] tab_name set 字段名=值
insert into employee_new set id=12,name="alvin3";
ON DUPLICATE KEY UPDATE的使用:
ON DUPLICATE KEY UPDATE c=VALUES(a)+b
1. 列名b表示原数据库的旧值,VALUES(b)表示引用被新插入的col_name的值
2. 可以使用if语法
on duplicate key update update_time=if(update_time>values(update_time),update_time,values(update_time))
3. 可以使用case when语法
on duplicate key update created_at=case when created_at<values(created_at) then values(created_at)
when created_at>=values(created_at) then created_at end
# 可以使用其他字段名称
# on duplicate key update level=values(level), update_time=case when level<>values(level) then values(update_time) else update_time end
# 这种写法,先更新了level,那么后面的语法查到的level与values(level)都是update后的同一个level
4. update level=values(level)如果新旧值不一样,才会去更新,才触发触发器
需要注意一下多个update值是否都更新了,如update_time
5. 需要避免的使用情况
a. 最好values括号内不要加新括号,否则如果values后面任意的一个()内再有)的话,会一直匹配到该处的)
b. 如要添加,确保后面的一个()没有子括号
UPDATE:
update tab_name set field1=value1,field2=value2,......[where 语句]
DELETE:
delete from tab_name [where ....]
SELECT:
查询表达式:
SELECT *|field1,filed2 ... FROM tab_name
WHERE 条件
GROUP BY field
HAVING 筛选
ORDER BY field
LIMIT 限制条数
OFFSET
UNION 去重组合 # UNION ALL 不管重复,上下并接
WHERE:
IN:
语法:
IN ()
子查询:
select * from table where uname in(select uname from user);
注意事项:
1.使用not in时,数据集不能有null,否则查询结果为null,自己写数据集时需注意。
2.通过pymyql连接时,推荐写法
"""select * from table where uid in %s""",(uids,) # 要加逗号,如果list为空,改为[""]的形式
返回字符串,这里一定要将字符串用单引号''单个 标注起来;
"""select * from table where uid in (%s)""", (",".join(str(u) for u in uids));
严禁在in内用两个单引号,会遇到sql语句错误,如下
select * from table where uid in('1,2,3');
或者
"select phone_number from installmentdb.t_user where uid in (%s)", (uids,)
LIKE:
语法:
'%a' //以a结尾的数据
'a%' //以a开头的数据
'%a%' //含有a的数据
'_a_' //三位且中间字母是a的
'_a' //两位且结尾字母是a的
'a_' //两位且开头字母是a的
如何优化like模糊查询:
联合索引+最左匹配:
速度还可以
非最左匹配:
因为后缀一般是没有优化的,把后缀裁剪出来,对后缀进行反转,然后对该生成列建索引,oracle对这种索引叫表达式索引,MySQL5.7成为生成列索引
create table t1(a int, b varchar(100), c varchar(10) as (reverse(substr(b, length(b)-10))));
create index i1 on t1(c);
这个时候在查询:
select * from t1 where c like reverse('%关键字'); //变为 字键关%
上es也可以
覆盖索引+JOIN:
对模糊搜索的字段建立普通索引,辅助索引idx_nickname(nickname)内部是包含主键id的,等价于(id,nickname)的复合索引
尝试利用覆盖索引特性将SQL改写为select Id from users01 where nickname like '%SK%'。
索引全扫描,但是需要的数据都在索引列中能找到,不需要回表。然后再与原表进行JOIN,拿出想要的数据。
效果:
减小了磁盘IO。
全文索引:
生成列使用(类似反转的做法)
利用内置reverse函数添加虚拟生成列,以空间换时间。
逻辑运算符:
and or not
比较运算符:
> < >= <= <> !=
BETWEEN AND:
ORDER BY:
概述:
指定字段排序,需要注意fileSort的情况和解决方法。
GROUP BY:
概述:
对数据分组
还可以实现去重功能
distinct与group by去重效率比较:
大数据的数据库:
distinct和group by都需要进行map以及reduce
一般情况下,使用distinct会将所有的order_no都shuffle到一个reducer里面,产生了数据倾斜(内存不足溢出到磁盘)。而group by分组,多个并行度。
如果重复多,去重后值比较少,那么distinct更快
spark的distinct:
println(myRDD.toDebugString)可以分析执行计划。
简单实验发现group by更快。
spark.read.orc(level2CoefPath).select($"level", $"coef").rdd.distinct().toDebugString
spark.read.orc(level2CoefPath).select($"level", $"coef").rdd.map(row=>((row.getString(0), row.getDouble(1)),1)).groupByKey().map(x=>(x._1,x._2)).toDebugString
发现两者的输出一样。
(5) MapPartitionsRDD[45] at distinct at <console>:25 []
| ShuffledRDD[44] at distinct at <console>:25 []
+-(5) MapPartitionsRDD[43] at distinct at <console>:25 []
| MapPartitionsRDD[42] at rdd at <console>:25 []
| SQLExecutionRDD[41] at rdd at <console>:25 []
| MapPartitionsRDD[40] at rdd at <console>:25 []
| MapPartitionsRDD[39] at rdd at <console>:25 []
| MapPartitionsRDD[38] at rdd at <console>:25 []
| FileScanRDD[37] at rdd at <console>:25 []
sql2.4.5下
调用sparkSession.sql("SELECT COUNT(DISTINCT(login)) FROM users").explain(true)发现两者的plan是一样的。
clickhouse:
group by的stage有Aggregating,而distinct有 Distinct和Distinct (Preliminary DISTINCT),比较不出来。
hive:
两者都是只有一个reducer stage,Reducer 2 <- Map 1 (SIMPLE_EDGE),执行计划一样。
mysql:
group by根据某个column对数据进行分组排序,建立临时表。(explain查看type为index,Using index; Using temporary; Using filesort)
distinct是取集合放到内存,比较key的hash(查看type为index,Using index; Using temporary;)
索引字段:group by和distinct一样,因为树只有一颗
非索引字段:distinct更快
推荐distinct,意图明显,而且优化器可能会做优化。
聚合函数:
如COUNT(列名)、SUM(列名)、AVG(列名)、Max、Min
LIMIT与OFFSET:
语法:
[OFFSET m] LIMIT n
注意事项:
超大分页问题。
多表查询:
连接查询JOIN:
笛卡尔积查询:
SELECT * FROM employee,department; --employee的每一行都对应department的所有行,没有on,只能where
内连接:
查询两张表中都有的关联数据,相当于利用条件从笛卡尔积结果中筛选出了正确的结果。
select * from employee,department where employee.dept_id = department.dept_id;
select * from employee inner join department on employee.dept_id = department.dept_id;
外连接:
左外连接:在内连接的基础上增加左边有右边没有的结果
select * from employee left join department on employee.dept_id = department.dept_id;
右外连接:在内连接的基础上增加右边有左边没有的结果
select * from employee RIGHT JOIN department on employee.dept_id = department.dept_id;
全外连接:在内连接的基础上增加左边有右边没有的和右边有左边没有的结果
mysql不支持全外连接 full JOIN,可以间接实现全外连接
select * from employee RIGHT JOIN department on employee.dept_id = department.dept_id
UNION
select * from employee LEFT JOIN department on employee.dept_id = department.dept_id;
on和where的区别:
on 比where先执行,是生成临时表的时候使用的条件,where是生成之后过滤
多个join联表顺序:
explain可以查看
联表顺序,不是两两联合之后,再去联合第三张表,而是驱动表的一条记录穿到底,匹配完所有关联表之后,再取驱动表的下一条记录重复联表操作;
驱动表:
第一个被处理的表,使用此表的记录去关联其他表。
算法:
Index Nested-Loop join:基于被驱动表的索引进行连接的算法;驱动表的记录逐条与被驱动表的索引进行匹配,避免和被驱动表的每条记录进行比较,减少了对被驱动表的匹配次数
Block Nested-Loop join 和 Batched Key Access join:每次从驱动表筛选一批数据,而不是一条。
hive:
只有一个reducer stage,多个Map任务执行TableScan,然后Merge Join Operator
Reducer 2 <- Map 1 (SIMPLE_EDGE), Map 3 (SIMPLE_EDGE), Map 4 (SIMPLE_EDGE),分别扫出多个表的记录,按照joined key进行group by shuffle,最后merge join
测试语句为:
create table a (id int,name string)partitioned by (dt string)
insert into a partition(dt='2021-01-01') values(1)
select * from a left join b on a.dt=b.dt and a.id=b.id left join c on a.id=b.id and a.dt=b.dt
clickhouse:
explain查看,会将前面的join结果保存为中间表(CreatingSets (Create sets for subqueries and joins)),再执行下一个join
left join只有join,而right join除了join还有Add non-joined rows after JOIN
子查询:
将一个查询语句嵌套在另一个查询语句中,内层查询语句的查询结果,可以为外层查询语句提供查询条件。
--子查询中可以包含:IN、NOT IN、ANY、ALL、EXISTS 和 NOT EXISTS等关键字
--还可以包含比较运算符:= 、 !=、> 、<等
a.带IN关键字的子查询
select * from employee where dept_id IN (select dept_id from department);
b.带比较运算符的子查询
select dept_id,dept_name from department where dept_id IN
(select DISTINCT dept_id from employee where age>=25);
c.带EXISTS关键字的子查询
--返回一个真假值。Ture或False。Ture时,外层查询语句将进行查询;False时,外层查询语句不进行查询。
select * from employee WHERE EXISTS (SELECT dept_name from department where dept_id=203);
索引:
概述:
创建和维护消耗时间和硬盘,但查询速度大大提高。索引会创建单独的文件存放
相关操作:
创建索引:
CREATE TABLE 表名(
字段名 类型[完整性约束条件],
[PRAMARY|UNIQUE|FULLTEXT|SPACIAL] INDEX|KEY [索引名] (字段名)
)
添加索引:
CREATE [PRAMARY|UNIQUE|FULLTEXT|SPACIAL] INDEX|KEY [索引名] (字段名)
ALTER TABLE 表名 ADD [PRAMARY|UNIQUE|FULLTEXT|SPACIAL]
|KEY [索引名] (字段名)
删除索引:
DROP INDEX 索引名 ON 表名
ALTER TABLE 表名 DROP INDEX 索引名
查看索引
show index from table_name;
主键:
概述:
一张表只能有一个主键,值唯一且不为空,unique and not null,可以设置自增auto_increment
删除主键:
alter table users drop primary key;
--主键唯一,自增必须为主键。先去自增,再删主键。
alter table test modify id int; -- auto_increment没了,但这样写主键依然存在,所以还要加上下面这句
alter table test drop primary key;-- 仅仅用这句也无法直接删除主键。
多字段联合主键:
primary key(name,id),主键类型不一定非是整型,如果是多列,则其组合必须唯一。
适合场景:
一般使用自增id作为主键。
外键:
限制:
要和关联主键的数据类型保持一致,默认名字:子表_ibfk_1(show create table table_name)
添加外键:
charger_id TINYINT,
[CONSTRAINT fk_name] FOREIGN KEY (charger_id) REFERENCES ClassCharger(id)
修改外键:
ALTER TABLE student ADD CONSTRAINT abc
FOREIGN KEY(charger_id)
REFERENCES classcharger(id);
删除外键:
ALTER TABLE student DROP FOREIGN KEY abc;
INNODB支持的ON语句:
外键约束对子表的含义:如果在父表中找不到候选键,则不允许在子表上进行insert/update
外键约束对父表的含义:创建外键关系时,可指定:
--外键的级联删除:如果父表中的记录被删除,则子表中对应的记录自动被删除
FOREIGN KEY (charger_id) REFERENCES ClassCharger(id) ON DELETE CASCADE
--在父表上update/delete记录时,将子表上匹配记录的列设为null,前提该外键列不能为not null
ON DELETE SET NULL
--Restrict方式:拒绝对父表进行删除更新操作(了解)
--No action方式:在mysql中同Restrict,如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作(了解)
分类:
一对一外键:
外键字段加UNIQUE约束即可
一对多:
外键字段不加UNIQUE约束
多对多:
两个实体都建成独立的主表, 另外再单独建一个关系表(采用唯一主键)
数据库中为什么不推荐使用外键约束?
外键的好处:
保证数据的完整性和一致性
级联操作方便
将数据完整性判断托付给了数据库完成,减少了程序的代码量
缺点:
1.性能问题
假设一张表名为user_tb。那么这张表里有两个外键字段,指向两张表。那么,每次往user_tb表里插入数据,就必须往两个外键对应的表里查询是否有对应数据。
如果交由程序控制,这种查询过程就可以控制在我们手里,可以省略一些不必要的查询过程。但是如果由数据库控制,则是必须要去这两张表里判断。
2.并发问题
在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。
3.扩展性问题
做平台迁移方便,比如你从Mysql迁移到Oracle,像触发器、外键这种东西,都可以利用框架本身的特性来实现,而不用依赖于数据库本身的特性,做迁移更加方便。
分库分表方便,在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。
4.技术问题
使用外键,其实将应用程序应该执行的判断逻辑转移到了数据库上。那么这意味着一点,数据库的性能开销变大了,那么这就对DBA的要求就更高了。
很多中小型公司由于资金问题,并没有聘用专业的DBA,因此他们会选择不用外键,降低数据库的消耗。
相反的,如果该约束逻辑在应用程序中,发现应用服务器性能不够,可以加机器,做水平扩展。如果是在数据库服务器上,数据库服务器会成为性能瓶颈,做水平扩展比较困难。
普通索引:
概述:
非聚簇索引
额外建立索引数,非覆盖索引的情况下需要回表IO操作
语法:
INDEX index_name(name)
唯一索引:
语法:
UNIQUE INDEX index_name(name)
与主键的比较:
1.主鍵一定是唯一性索引,唯一性索引並不一定就是主鍵。
2.一個表中可以有多個唯一性索引,但只能有一個主鍵。
3.主鍵列不允許空值,而唯一性索引列允許空值。
全文索引:
概述:
一般给大文本索引
语法:
FULLTEXT INDEX index_name(name)
联合[唯一]索引:
概述:
比逐个创建效果高,查询效率低,多个列查询时共享一个索引
语法:
INDEX index_name(name,resume)
底层结构:
一棵B+树。
第一种说法:索引值为多列值元组,判断的时候走多列判断。
第二种说法:嵌套树。一个树类型包含多个字段,a本身是对应的val,而b,c对应树
优点:
对于同时搜索n个条件时,组合索引的性能好于多个单一索引合并(多索引只能使用一个)。
应用场景:
频繁的同时使用n列来进行查询。
最左前缀原则:
概述:
使用索引时必须按照从左到右的顺序依次包含,否则不命中。
注意事项:
1.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。
2.最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。a>1and b=2,a字段可以匹配上索引,但b值不可以,因为a的值是一个范围,在这个范围中b是无序的。
原因:
因为MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。
索引的形式:(1,2) (1,3)这种。
生产示例:
1.SELECT * FROM table WHERE a = 1 and b = 2 and c = 3; 如何建索引?
(a,b,c)或者(c,b,a)或者(b,a,c)都可以,重点要的是将区分度高的字段放在前面,区分度低的字段放后面。像性别、状态这种字段区分度就很低,我们一般放后面。
索引优化:
多索引查询优化:
1. key1 and key2,多个普通索引只会用其中一个!
当多条件联合查询时,优化器会评估用哪个条件的索引效率最高!它会选择最佳的索引去使用。
如果某个为主键,type将为const。
+----+-------------+-------+------------+------+---------------------+-----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+-----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | test | NULL | ref | indexkey1,indexkey2 | indexkey1 | 4 | const | 1 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------------+-----------+---------+-------+------+----------+-------------+
2. key1 or key2,explain的类型为index_merge(5.6版本和8.0版本都这样)(数据量少的情况下,type为ALL)。将查询结果合并。
explain select * from `test` where `key1`=1 or `key2`=1;
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | test | NULL | ALL | indexkey1,indexkey2 | NULL | NULL | NULL | 1 | 100.00 | Using where |
+----+-------------+-------+------------+------+---------------------+------+---------+------+------+----------+-------------+
索引下推:
背景:
在未开启 ICP 的情况下,存储引擎并未利用索引上的 b 值进行判断。而是进行回表查询,将a>1的所有数据读出、返回至 MySQL Server 层,由 Server 通过Using where根据b=2筛选目标记录。
这一过程,利用Table Filter
概述:
MySQL 在5.6版本后加入该功能。
开启 ICP,查看执行计划时,Extra 字段会有Using index condition说明,表示 ICP 生效,减少了回表数据。
这会改善 IO 操作数,提升处理效率。
注意:
1.ICP 适用于range(常数值的范围), ref(只匹配少数行), eq_ref(只在该表读取一行), and ref_or_null(与ref类似,但包括NULL)的回表操作前过滤数据。
减小回表数据量
2.支持InnoDB和MyISAM引擎
3.ICP 目的是减少回表读操作数(reduce the number of full-row reads),从而减少 I/O 操作
4.InnoDB中 ICP 仅支持二级索引,不支持聚簇索引。因InnoDB引擎下,聚簇索引的字段信息已全部在索引中。
5.指向子查询的查询条件无法利用 ICP
6.函数(存储过程)或触发器无法利用 ICP
适合场景:
索引下推一般可用于所求查询字段(select列)不是/不全是联合索引的字段,查询条件为多条件不等值查询且查询条件子句(where/order by)字段全是联合索引。
例如a= b>。
为何要求查询字段不是/不全是联合索引的字段?
因为innodb的主键索引树叶子结点上保存的是全行数据,所以这个时候索引下推并不会起到减少查询全行数据的效果。
谓词下推:
概述:
在SQL中,谓词就是返回boolean值即true或者false的函数,或是隐式转换为boolean的函数。SQL中的谓词主要有 LKIE、BETWEEN、IS NULL、IS NOT NULL、IN、EXISTS
将外层查询块的 WHERE 子句中的谓词移入所包含的较低层查询块(例如视图,join的on条件),从而能够提早进行数据过滤以及有可能更好地利用索引。
主流数据库都支持
基本思想:
始终将过滤表达式尽可能移至靠近数据源的位置。
覆盖索引:
概述:
直接SQL只需要通过索引就可以返回查询所需要的数据,而不必通过二级索引查到主键之后再去查询数据。
能够减少树的搜索次数,避免了回表,显著提升了查询性能,因此覆盖索引是一个常用的性能优化手段。
即叶子节点除了保存该行的键值还保存了对应索引列的值,如果不需要额外数据的话则不需要另外对聚集索引中的数据进行 IO
适合场景:
遇到以下情况,执行计划不会选择覆盖查询
1.select选择的字段中含有不在索引中的字段 ,即索引没有覆盖全部的列。
2.where条件中不能含有对索引进行like的操作。
无法命中的几种情况:
like\使用函数\or有未建立索引列\传入类型不一致\!=字段(除主键外)\使用>(除主键或整数索引外)\order by非索引列\不遵循组合索引最左前缀
聚簇索引与非聚簇索引:
聚簇索引:
innodb 引擎默认在主键上建立聚簇索引,通常说的主键索引就是聚簇索引,聚簇索引会保存行上的所有数据,因此不需要额外的 IO
非聚簇索引:
辅助索引 (Secondary Index):叶子节点只保存了行的键值和对应的主键索引
区别:
聚簇索引的叶子节点存放的是整行数据,非聚簇索引的叶子节点存放的是主键的值。
相同点:
两者都是B+树,非叶子节点不存具体信息,只进行数据索引。
B树与B+树:
区别:
非叶子节点是否保存关键字记录的指针
比较:
1.B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加。
结论:
B+树层级更少,空间利用率更高,可减少I/O次数,磁盘读写代价更低。
2.B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
查询单条数据的时候,B树的查询效率不固定,最好的情况是O(1)。可以认为在做单一数据查询的时候,使用B树平均性能更好。
把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。
结论:
B树适合Mongodb这种做单一查询比较多,数据遍历操作比较少的非关系型场景。
B+树查询效率比较稳定
3.B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针,所有叶子节点都是通过指针连接在一起,而B树不会。
所以B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
结论:
B+树遍历、增删更快。
其他数据结构:
哈希表:
不适合遍历,无法排序,
只支持等值查询(不能范围查询)
不支持模糊查询以及多列索引的最左前缀匹配(AAAA和AAAAB的索引没有相关性。)
存在hash冲突(维护代价高,查询速度下降)
不能避免回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询。
红黑树:
1.B+树是为磁盘或其他直接存取的辅助存储设备而设计的一种数据结构。
2.磁盘IO才是性能瓶颈的关键,所以我们需要的是减少树的深度,所以我们需要更多分叉的树 ,还需要更适合磁盘操作特性的数据结构。
3.红黑树增删需要自旋操作来平衡。
跳表:
1.跳跃表不适用于磁盘读取的场景
B+树的页天生就和磁盘块对应,索引按照文件的形式存放
量级大的时候,跳跃表的level太高,数据存储不紧凑,产生大量的空间浪费,插入的数据不会如b+树那么紧凑,数据的压缩,dump也会存在问题。查询会产生大量跨页IO。
查询时候磁盘磁头无法对链表进行预读,会产生大量的随机IO,对磁盘的缓存不友好。
2.跳跃表的查找效率不如B+树效率高,也不如B+树稳定。
log2n
O(m*logn)
redis为什么用跳表?
1.在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。
如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
2.平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
3.从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。
如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
4.从算法实现难度上来比较,skiplist比平衡树要简单得多。
官方提到原因:从内存占用、对范围查找的支持和实现难易程度这三方面总结的原因
视图:
概述:
视图是一个虚拟表(非真实存在),其本质是【根据SQL语句获取动态的数据集,并为其命名】,用户使用时
只需使用【名称】即可获取结果集,并可以将其当作表来使用。简单说就是临时表,视图存在数据库。
相关操作:
1、创建视图
CREATE VIEW v1 AS SELET nid,name FROM A WHERE nid > 4
2、删除视图
DROP VIEW v1
3、修改视图
ALTER VIEW v1 AS
4、使用视图
由于视图是虚拟表,所以无法使用其对真实表进行创建、更新和删除操作,仅能做查询用。
select * from v1
触发器:
1、基本语法
a.插入前
CREATE TRIGGER tri_before_insert_tb1 BEFORE INSERT ON tb1 FOR EACH ROW
BEGIN
...
END
b.插入后
CREATE TRIGGER tri_after_insert_tb1 AFTER INSERT ON tb1 FOR EACH ROW
c.删除前
CREATE TRIGGER tri_before_delete_tb1 BEFORE DELETE ON tb1 FOR EACH ROW
d.删除后
CREATE TRIGGER tri_after_delete_tb1 AFTER DELETE ON tb1 FOR EACH ROW
e.更新前
CREATE TRIGGER tri_before_update_tb1 BEFORE UPDATE ON tb1 FOR EACH ROW
f.更新后
CREATE TRIGGER tri_after_update_tb1 AFTER UPDATE ON tb1 FOR EACH ROW
示例:
delimiter // # 换个终止符
CREATE TRIGGER tri_before_insert_tb1 BEFORE INSERT ON userinfo FOR EACH ROW
BEGIN
IF NEW.username == 'alex' THEN # NEW代表新插入的一行数据
INSERT INTO tb1 (NAME)
VALUES
(NEW.username); # 记得加分号
END//
delimiter ;
# insert into userinfo (username,password,nickname) values('alex',123,'chen')
特别的:NEW表示即将插入的数据行,OLD表示即将删除的数据行。
2、删除触发器
DROP TRIGGER tri_after_insert_tb1;
3、使用触发器
--触发器无法由用户直接调用,而知由于对表的【增/删/改】操作被动引发的。
insert into tb1(num) values(666)
4、查看触发器
自定义函数:
定义:
delimiter \\
create function f1(
i1 int,
i2 int)
returns int
BEGIN
declare num int;
set num = i1 + i2;
return(num);
END \\
delimiter ;
删除函数:
drop function func_name;
使用:
select f1(11,nid) ,name from tb2;
查看函数:
show create function f1;
快速插入数据示例:
delimiter $$
create function rand_string(n int) returns varchar(255)
begin
declare chars_str varchar(100) default 'asdasdasd';
declare return_str varchar(255) default '';
declare i int default 0;
while i<n do set return_str=concat(return_str,substring(chars_str,floor(1+rand()*9),1));
set i=i+1;
end while;
return return_str;
end $$
create function rand_number() returns int(5)
begin
declare i int default 0;
set i=floor(100+rand()*10);
return i;
end $$
create procedure insert_test(in start int(10),in max_num int(10))
begin
declare i int default 0;
set autocommit=0;
repeat
set i=i+1;
insert into test values(start+i,rand_number(),rand_string(6));
until i=max_num end repeat;
commit;
end $$
delimiter;
存储过程:
概述:
存储过程是一个SQL语句集合,当主动去调用存储过程时,其中内部的SQL语句会按照逻辑执行。
对结果集合返回值进行事务+复杂操作。全部成功返回0,部分失败返回1,全部失败返回2
与自定义函数的区别:存储过程能执行sql语句,返回值通过select获取
示例:
创建多行数据
delimiter $$ --更改结束符
create procedure autoinsert(in num int)
BEGIN
declare i int default 1; --declare声明,set设置值
while(i<num)do --类似的函数有if,repeat until,loop_label:loop
insert into test.A values(i,'yuan');
set i = i+1;
end while;
END$$
delimiter ; --改回来
call autoinsert(1000); 调用该存储过程。
执行:
set @t1 =4;
set @t2 = 0; # out类型,可以为0
CALL p1 (1, 2 ,@t1, @t2); # 会执行内部的sql语句,立即返回结果,而out通过select获取
SELECT @t1,@t2;
传参:
对于存储过程,可以接收参数,其参数有三类:
in 仅用于传入参数用
out 仅用于返回值用,传参进去运算后通过select out;即可获取
inout 既可以传入又可以当作返回值
删除:
drop procedure proc_name;
与函数的区别:
1.一般来说,存储过程实现的功能要复杂一点,而函数的实现的功能针对性比较强。存储过程,功能强大,可以执行包括修改表等一系列数据库操作;用户定义函数不能用于执行一组修改全局数据库状态的操作。
2.函数只能返回一个变量的限制。而存储过程可以返回多个。
3.函数限制比较多,比如不能用临时表,只能用表变量.还有一些函数都不可用等等.而存储过程的限制相对就比较少。
4.存储过程一般是作为一个独立的部分来执行( EXECUTE 语句执行),而函数可以作为查询语句的一个部分来调用(SELECT调用),由于函数可以返回一个表对象,因此它可以在查询语句中位于FROM关键字的后面。
SQL语句中不可用存储过程,而可以使用函数。
事务:
相关命令:
BEGIN 开启事务
Rollback 回滚事务,即撤销指定的sql语句(只能回退insert delete update语句),回滚到上一次commit的位置
Commit 提交事务,提交未存储的事务
savepoint 保留点 ,事务处理中设置的临时占位符 你可以对它发布回退(与整个事务回退不同)
ACID特性:
原子性(Atomicity):
概述:
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
原理:
mysql利用Innodb的undo log
undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
一致性(Consistency):
概述:
事务前后数据的完整性必须保持一致。
在事务执行之前数据库是符合数据完整性约束的,无论事务是否执行成功,事务结束后的数据库中的数据也应该是符合完整性约束的。
在某一时间点,如果数据库中的所有记录都能保证满足当前数据库中的所有约束,则可以说当前的数据库是符合数据完整性约束的。
原理:
从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。
数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。
隔离性(Isolation):
概述:
事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
原理:
至于MVCC,即多版本并发控制(Multi Version Concurrency Control),一个行记录数据有多个版本对快照数据,这些快照数据在undo log中。
如果一个事务读取的行正在做DELELE或者UPDATE操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。
持久性(Durability):
概述:
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
原理:
利用Innodb的redo log。
当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。
当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
隔离级别:
可能出现的问题:
脏读:--一个事务读取到了另一个事务未提交的数据,这是特别危险的,要尽力防止。
不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。
虚读:在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
四个隔离级别:
Serializable:
概述:
在该隔离级别下事务都是串行顺序执行的,该级别下读写串行化,且所有的select语句后都自动加上lock in share mode,即使用了共享锁。
MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
因此在该隔离级别下,使用的是当前读,而不是快照读。
Repeatable read:
概述:
该隔离级别是 MySQL 默认的隔离级别。
在同一个事务里, select 的结果是事务开始时时间点的状态(快照读),因此,同样的 select 操作读到的结果会是一致的。
优点:
解决了不可重复读的问题。
缺点:
1.会有 幻读 现象,不可重复读是读异常,但幻读则是写异常。
幻读可以通过手动加行锁解决,gap lock可以防止重复写。
2.由于gap lock的存在,条件列未命中索引的情况下会锁大部分范围or整个表(条件列非索引)
Read committed:
概述:
一个事务可以读取另一个已提交的事务
优点:
可避免脏读情况发生(读已提交)
缺点:
1.多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
避免不可重复读主要靠一致性快照。
2.不能使用STATEMENT格式的binlog,否则会出现主从不一致的问题。
在master上执行事务的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!
Read uncommitted:
概述:
最低级别,以上情况均无法保证。(读未提交) 该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为 脏读 。
安全性考虑:
Serializable>Repeatable read>Read committed>Read uncommitted
数据库效率:
Read uncommitted>Read committed>Repeatable read>Serializable
互联网项目多用读已提交(Read Commited)这个隔离级别:
原因:
1.在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!
2.在RR隔离级别下,条件列未命中索引会锁表(有待商榷)!而在RC隔离级别下,只锁行。MySQL做了优化,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁。
3.在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!
在5.1.15的时候,innodb引入了一个概念叫做“semi-consistent”,减少了更新同一行记录时的冲突,减少锁等待。
所谓半一致性读就是,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。
若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)!
而在RR隔离级别下,Session2只能等待!
4.在RC级别下,不可重复读问题不需要解决,毕竟你数据都已经提交了,读出来本身就没有太大问题。只是用的binlog为row格式
原因:
statement下从库的事务按照commit的顺序执行。
相关操作:
设置数据库的隔离级别:
set [global/session] transaction isolation level xxxx;
查看:
select @@global.transaction_isolation,@@transaction_isolation;
快照读和当前读:
快照读:
select * from table where id = ?;读的是数据库记录的快照版本,是不加锁的。
当前读:
加了lock in share mode和for update
锁:
表锁:
LOCK TABLE … READ 对MyISAM表的读操作 (加读锁) ,不会阻塞其他进程对同一表的读请求,但是会阻塞对同一表的写请 求. 只有当读锁释放后,才会执行其他进程的写操作.
LOCK TABLE … WRITE 对MyISAM表加写锁, 会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其他进 程的操作
UNLOCK
行锁上升为的表锁
行锁:
前提:
1.仅适用于InnoDB
2.必须在事务处理模块(BEGIN/COMMIT)中才能生效。
分类:
共享锁(S锁):假设事务T1对数据A加上共享锁,那么事务T2可以读数据A,不能修改数据A。
排他锁(X锁):假设事务T1对数据A加上排他锁,那么事务T2不能读数据A,不能修改数据A。
意向共享锁(IS锁):一个事务在获取(任何一行/或者全表)S锁之前,一定会先在所在的表上加IS锁。可以阻塞表写锁
意向排他锁(IX锁):一个事务在获取(任何一行/或者全表)X锁之前,一定会先在所在的表上加IX锁。可以阻塞表写读锁
InnoDB引擎默认更新语句,update,delete,insert 都会自动给涉及到的数据加上排他锁,
行锁的范围分类:
Record Locks:对索引记录进行加锁!锁是在加索引上而不是行上的。注意了,innodb一定存在聚簇索引,因此行锁最终都会落到聚簇索引上!
Gap Locks:简单翻译为间隙锁,是对索引的间隙加锁,其目的只有一个,防止其他事物插入数据。
在Read Committed隔离级别下,不会使用间隙锁。这里我对官网补充一下,隔离级别比Read Committed低的情况下,也不会使用间隙锁,如隔离级别为Read Uncommited时,也不存在间隙锁。
当隔离级别为Repeatable Read和Serializable时,就会存在间隙锁。
Next-Key Locks:这个理解为Record Lock+索引前面的Gap Lock。记住了,锁住的是索引前面的间隙!范围为(无穷小或小于表中锁住id的最大值,无穷大或大于表中锁住id的最小值)
场景分析:
1.RC/RU+条件列非索引
不加锁时是快照读,lock in share mode/for update都是当前读,只在条件匹配后的对应记录的聚簇索引上加对应的锁。
2.RC/RU+条件列是聚簇索引
不加锁时是快照读,lock in share mode/for update都是当前读,只在条件匹配后的对应记录的聚簇索引上加对应的锁。
区别:
在条件列没有索引的情况下,尽管通过聚簇索引来扫描全表,进行全表加锁。但是,MySQL Server层会进行过滤并把不符合条件的锁当即释放掉,因此你看起来最终结果是一样的。
但是RC/RU+条件列非索引比本例多了一个释放不符合条件的锁的过程!
3.RC/RU+条件列是非聚簇索引
不加锁时是快照读,lock in share mode/for update都是当前读,在条件匹配后的对应记录的非聚簇索引+聚簇索引上加对应的锁。
=============================================================================================================================
4.RR/Serializable+条件列非索引
不加锁时,无论条件是=还是>,RR是快照读,Serializable在全表所有记录的聚簇索引上加S锁,并且在聚簇索引的所有间隙加gap lock
加锁时,在全表所有记录的聚簇索引上加对应的锁,并且在聚簇索引的所有间隙加gap lock
5.RR/Serializable+条件列是聚簇索引
不加锁时,RR是快照读,而Serializable,如果是=,在对应聚簇索引上加S锁;如果是>,在对应的聚簇索引上加S锁,在条件值往后的所有间隙加上gap lock
加锁时,
如果是=,在对应聚簇索引上加对应的锁,不存在gap lock。
如果是>,在对应聚簇索引上加对应锁,在条件值往后的所有间隙加上gap lock。<的情况是往前,查看锁可以看到lock_data为该值,type为X,GAP的锁。
如果记录不存在 + =的情况下,在条件值所在的间隙加上gap lock。
如果记录不存在 + > 的情况下,在条件值前面的索引值开始,往后的所有间隙加上gap lock。<的情况相反。
6.RR/Serializable+条件列是非聚簇索引
如果是唯一索引,情况和RR/Serializable+条件列是聚簇索引类似,唯一有区别的是:这个时候有两棵索引树,加锁是加在对应的非聚簇索引树和聚簇索引树上!
如果是非唯一索引,通过索引进行精确查询以后,不仅存在record lock,还存在gap lock。而通过唯一索引进行精确查询后,只存在record lock,不存在gap lock。
不加锁时,
RR是快照读,
而Serializable,
如果是=,在对应的非聚簇和聚簇索引上加S锁,同时在非聚簇索引的前后两个间隙加上gap lock。
如果是>,在对应的非聚簇和聚簇索引上加S锁,同时在非聚簇索引的往后所有间隙加上gap lock。
加锁时,
如果是=,在对应的非聚簇和聚簇索引上加对应锁,同时在非聚簇索引的前后两个间隙加上gap lock。
如果是>,在对应的非聚簇和聚簇索引上加对应锁,同时在非聚簇索引的往后所有间隙加上gap lock。
如果记录不存在 + =的情况下,在条件值所在的非聚簇索引间隙加gap lock。
如果记录不存在 + > 的情况下,在条件值往后的所有非聚簇索间隙加上gap lock。
锁表的情况(不是表锁,而是Next-Key Locks覆盖了全表):
1.RR+条件列非索引+加锁时、Serializable+条件列非索引。
2.FOR UPDATE无主键。
前提是事务级别为RR和Serializable,如果隔离级别为RU和RC,无论条件列上是否有索引,都不会锁表,只锁行!
查看行锁情况:
show processlist;
show OPEN TABLES where In_use > 0;
select * from information_schema.innodb_trx\G; 运行事务
select * from information_schema.innodb_locks\G;
show status like 'innodb_row_lock_%'; 行锁
select * from performance_schema.data_locks\G 查看哪些行被锁了。
select * from performance_schema.data_lock_waits\G
set innodb_lock_wait_timeout=600
死锁产生的原因和解决:
现象:
一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;
一把锁,进程获取后panic异常,没有释放。
原因:
1.事务之间对资源(表或者行)访问顺序的交替
一个用户A 访问表A(锁住了表A),然后又访问表B;另一个用户B 访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。
解决:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。
建议:如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会;
具体场景:
select * from t3 where id in (8,9) for update;
select * from t3 where id in (10,8,5) for update;
select * from t3 where id=5 for update;
select * from t3 where id=10 for update;
2.并发修改同一记录
用户A查询一条纪录for share,(进行一些业务操作),然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,
而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。
出现场景:
1.如在某项目中,页面上的按钮点击后,没有使按钮立刻失效,使得用户会多次快速点击同一按钮,这样同一段代码对数据库同一条记录进行多次操作,很容易就出现这种死锁的情况。
解决:对于按钮等控件,点击后使其立刻失效,不让用户重复点击,避免对同时对同一条记录操作。
2.先查询是否有这条记录,用for update,然后再写入。
解决:应该避免这种操作,会造成gap lock。而是使用on duplicate key update
解决:
使用乐观锁进行控制。
使用悲观锁进行控制。
最新版8.0尝试:
B会立刻失败,接着A获取到了X锁。
3.索引不当导致全表扫描
如果在事务中执行了一条不满足条件的语句,执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行后,就很容易产生死锁和阻塞。
解决:
SQL语句中不要使用太复杂的关联多表的查询;
建议:
(1)如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会;
(2)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
(3)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。
乐观锁、悲观锁:
概述:
乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
乐观锁:
概述:
乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
适合场景:
写比较少的情况下(多读场景)即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
实现:
乐观锁一般会使用版本号机制或CAS算法实现。
版本号机制:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。
当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
具体操作:
SELECT name AS old_name, version AS old_version FROM tb where ...
根据获取的数据进行业务操作,得到new_dname和new_version
begin;
update tb set name='yyy' and version=version+1 where id=1 and version=version;是否更新可以通过获取返回的修改行数。
进行业务操作
判断是否insert into xxx 订单。
commit;
CAS算法:
即compare and swap(比较与交换),是一种有名的无锁算法。
无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
缺点:
1、ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,
那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
解决:
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,
如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2、循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
3、只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.
所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
悲观锁:
概述:
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
适合场景:
多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
MVCC、undo、redo、binlog、二阶段提交:
前置概念:
1.数据库中,数据在内存中叫data buffer,数据在磁盘上叫data file。
事务的日志也一样,在内存中叫log buffer,在磁盘上叫log file。
2.data buffer中的数据会在合适的时间 由存储引擎写入到data file。并不在事务提交时机制中。
3.checkpoint:
checkpoint是为了定期将db buffer的内容刷新到data file。
当遇到内存不足、db buffer已满等情况时,需要将db buffer中的内容/部分内容(特别是脏数据)转储到data file中。
在转储时,会记录checkpoint发生的”时刻“。在故障恢复时候,只需要redo/undo最近的一次checkpoint之后的操作。
一、redo log:
1.概念
记录事务执行后的状态,用来恢复未写入磁盘的数据(data file)的已成功事务更新的数据。
例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Redo日志为<T1, X, 15>。
大多数是物理日志。
2.作用
1.保证了事务的持久性。
2.数据库崩溃时回复。如:A字段本来=1,设置A字段=5,内存中的数据data buffer已经改了,redo log也刷入磁盘中了,事务也提交了,但是还没来及的把A字段=5刷到磁盘中,数据库就挂了,那再恢复了数据库看到的A字段就是=1,这时就用redo log来回放把A变成=5
3.流程
a.事务开启
b.数据读到data buffer,进行修改
c.修改后产生redo log buffer
d.redo log buffer不断写入redo log file,写入完成后才提交事务
e.data buffer根据阈值不定期写入data file
这种先持久化日志的策略叫做Write Ahead Log,即预写日志。
二、undo:
1.概念
用于记录事务开始前的状态,用于事务失败时的回滚操作。
例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Undo日志为<T1, X, 5>。
逻辑日志
重点:
1.修改前做undo log记录
2.undo log刷入磁盘后,才提交事务。
2.作用
1.保证了事务的原子性(回滚)
2.实现MVCC(参考mvcc说明)
3.保证普通select快照读
3.UNDO LOG中分为两种类型
1. INSERT_UNDO(INSERT操作),记录插入的唯一键值;
2. UPDATE_UNDO(包含UPDATE及DELETE操作),记录修改的唯一键值以及old column记录。
4.流程
假设有A、B两个数据,值分别是 1 和 2,在一个事务中先后把A设置为3,B设置为4。
1.事务开始
2.记录A=1到undo log buffer
3.修改A=3
4.记录A=3到redo log buffer
5.记录B=2到undo log buffer
6.修改B=4
7.记录B=4到redo log buffer
8.到log buffer全部刷入到磁盘中后才提交数据
这里有个点,log buffer 刷入到磁盘,并不是最后要提交事物了才来一次性全部刷入到磁盘。log buffer刷入到log file是在事务进行的时候就逐步在做了。
三、恢复:
1.事务无法回滚,无法回放的情况 :
innodb_flush_log_at_trx_commit=2时,将redo日志写入logfile后,为提升事务执行的性能,存储引擎并没有调用文件系统的sync操作,将日志落盘。如果此时宕机了,那么未落盘redo日志事务的数据是无法保证一致性的。
undo日志同样存在未落盘的情况,可能出现无法回滚的情况。
2.未提交的事务和回滚了的事务也会记录Redo Log,因此在进行恢复时,这些事务要进行特殊的的处理。
有2种不同的恢复策略:
A. 进行恢复时,只重做已经提交了的事务。
B. 进行恢复时,重做所有事务包括未提交的事务和回滚了的事务。然后通过Undo Log回滚那些未提交的事务。
四、二阶段提交:
流程:
1.先进入commit prepare 阶段:事务中新生成的redo log 会被刷到磁盘,并将回滚段置为prepared状态。binlog不作任何操作。存储引擎写redo log
2.commit阶段:innodb释放锁,释放回滚段,设置redo log提交状态,binlog持久化到磁盘,然后存储引擎层提交。Server层写binlog
正常的2pc提交流程:
提交请求(投票)阶段
1.协调者向所有参与者发送prepare请求与事务内容,询问是否可以准备事务提交,并等待参与者的响应。
2.参与者执行事务中包含的操作,并记录undo日志(用于回滚)和redo日志(用于重放),但不真正提交。
3.参与者向协调者返回事务操作的执行结果,执行成功返回yes,否则返回no。
提交(执行)阶段
若所有参与者都返回yes,说明事务可以提交:
1.协调者向所有参与者发送commit请求。
2.参与者收到commit请求后,将事务真正地提交上去,并释放占用的事务资源,并向协调者返回ack。
3.协调者收到所有参与者的ack消息,事务成功完成。
若有参与者返回no或者超时未返回,说明事务中断,需要回滚:
1.协调者向所有参与者发送rollback请求。
2.参与者收到rollback请求后,根据undo日志回滚到事务执行前的状态,释放占用的事务资源,并向协调者返回ack。
3.协调者收到所有参与者的ack消息,事务回滚完成。
五、MVCC:
1.简介
MVCC的全称是“多版本并发控制”。这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证。
换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。
MVCC由于其实现原理,只支持read committed和repeatable read隔离等级
2.作用:
1.增强并发性。这样的一来查询就不用等待另一个事务释放锁。
2.在RR可重复读的隔离级别中,保障了单纯的select(不会加锁)的“可重复读”特性
这项技术在数据库领域并不是普遍使用的,其它的数据库产品,以及mysql其它的存储引擎并不支持它。
3.快照读和当前读
快照读:读取的是快照版本,也就是历史版本。如:普通的SELECT
当前读:读取的是最新版本,如:UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。
4.锁定读
在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。
SELECT ... LOCK IN SHARE MODE
给记录加设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交
SELECT ... FOR UPDATE
给记录加排它锁,这种情况下跟UPDATE的加锁情况是一样的
5.一致性非锁定读
事务中的单纯、标准的select读是不加锁的。
而这个单纯的select不加锁的读就是一致性非锁定读
一致性非锁定读就是MVCC来保证的
consistent read(一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。
1.如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;
2.如果是READ COMMITTED,那么一个事务中的每一个一致性读都会读到它自己刷新的快照版本。
Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ隔离级别下普通SELECT语句默认的模式。
一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们。
6.实现机制
1.InnoDB会给数据库中的每一行增加三个隐藏字段,它们分别是DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏的ID)。每开启一个新事务,事务的版本号就会递增。
此时开启一个事务A,对该条数据做修改,在修改前该条数据会做undo log 相当于有了一个未修改的副本,修改后DB_ROLL_PT会指向那个未修改的副本
2.MVCC 在mysql 中的实现依赖的是 undo log 与 read view
1.undo log: undo log中记录的是数据表记录行的多个版本,也就是事务执行过程中的回滚段,其实就是MVCC 中的一行原始数据的多个版本镜像数据。
2.read view: 主要用来判断版本链中哪个版本是当前事务可见的。
7.read view
概述:
主要用来判断版本链中哪个版本是当前事务可见的。
需要注意的是,新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表中。
ReadView中主要包含4个比较重要的内容:
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
creator_trx_id:表示生成该ReadView的事务的事务id。
在mysql的innodb中,ReadView的判断方式:
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
事务隔离级别:读已提交和可重复读在ReadView的区别:
1.读已提交时,事务中,每次读都生成一个新的ReadView
2.可重复读时,事务中,第一次读生成一个ReadView,该事务中以后的读是使用的同一个ReadView,就是第一次读生成的那个ReadView
数据导入与导出:
导出数据:
1.mysqldump
mysqldump -u [uname] -p[pass] db_name [tbl_name ...] > table_backup.sql # >> 是追加
mysqldump -u [uname] -p[pass] --databases db_name > table_backup.sql
mysqldump -u [uname] -p[pass] --all-databases > table_backup.sql
示例:
mysqldump -u feature -pakulaku_feature featuredatadb r_id_user_risk_level_dim --where="dim_value>0" >risk.sql
-h host
2.使用shell命令行outfile导出
select * from djangodb.web_timertask into outfile '/var/lib/mysql-files/timer2.sql' fields terminated by '|' enclosed by '"'
lines terminated by '\r\n' ;
导入数据:
1.mysql -u [uname] -p[pass] dbname < table_backup.sql
# 默认会drop table然后重新create,需要dump时加上--skip-add-drop-table和--no-create-info,这样才是追加
2.mysqlimport
实现的功能类似LOAD DATA INFILE,能够导入CSV等非mysqldump输出格式
3.load data infile
load data infile "/var/lib/mysql-files/timer3.sql" replace into table djangodb.web_timertask_2 fields terminated by '|' enclosed
by '"' lines terminated by '\r\n' (id,title,funcname);
Explain执行计划:
概述:
查询执行计划。
• EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
• EXPLAIN不考虑各种Cache
• EXPLAIN不能显示MySQL在执行查询时所作的优化工作
• 部分统计信息是估算的,并非精确值
• EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划。
示例:
image-20211222112639076+----+-------------+------------+-------+---------------+-------------+---------+--------------------------------------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+-------------+---------+--------------------------------------+----------+-------------+
| 1 | PRIMARY | x | ref | uid | uid | 8 | const | 1 | Using where |
| 1 | PRIMARY | <derived2> | ref | <auto_key0> | <auto_key0> | 91 | const,installmentdb.x.repayment_date | 10 | Using index |
| 2 | DERIVED | t_bill | index | NULL | uid | 91 | NULL | 15382205 | NULL |
+----+-------------+------------+-------+---------------+-------------+---------+--------------------------------------+----------+-------------+
各列的含义如下:
id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.
select_type: SELECT 查询的类型.
table: 查询的是哪个表
type: 访问类型
possible_keys: 此次查询中可能选用的索引
key: 此次查询中确切使用到的索引.
ref: 哪个字段或常数与 key 一起被使用
rows: 显示此查询一共扫描了多少行. 这个是一个估计值.
extra: 额外的信息
select_type查询类型:
select_type 表示了查询的类型, 它的常用取值有:
SIMPLE 表示此查询不包含 UNION 查询或子查询
PRIMARY 查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY
UNION 表示此查询是 UNION 的第二或随后的查询
DEPENDENT UNION UNION 中的第二个或后面的查询语句, 取决于外面的查询
UNION RESULT UNION 的结果
SUBQUERY 子查询中的第一个 SELECT
DEPENDENT SUBQUERY 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.
DERIVED (派生表的SELECT, FROM子句的子查询)
UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
table表名:
显示这一行的数据是关于哪张表的,有时不是真实的表名字,看到的是derivedx(x是个数字,我的理解是第几步执行的结果)
type访问类型:
表示MySQL在表中找到所需行的方式,又称“访问类型”。
常用的类型有: ALL、index、range、index_merge、ref_or_null、ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
ALL:
表示全表扫描, 这个类型的查询是性能最差的查询之一
index:
表示全索引扫描(full index scan), 和 ALL 类型类似, 只不过 ALL 类型是全表扫描, 而 index 类型则仅仅扫描所有的索引, 而不扫描数据.
index 类型通常出现在: 所要查询的数据直接在索引树中就可以获取到, 而不需要扫描数据. 当是这种情况时, Extra 字段 会显示 Using index.
range:
只检索给定范围的行,使用一个索引来选择行
表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中.
index_merge:
会分别查询两个索引的集合,然后取并集
可能的场景:key1=1 or key2=1
ref_or_null:
类似ref,但是可以搜索值为NULL的行。
比如S.`name` = "张飞" OR S.`name` IS NULL
ref:
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 最左前缀 规则索引的查询
eq_ref:
类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配
简单来说,就是多表连接中使用primary key或者unique key作为关联条件
此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较操作通常是 =, 查询效率较高
const、system:
当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,
system是const类型的特例,当查询的表只有一行的情况下,使用system
NULL:
MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
possible_keys可能索引:
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
即使有些索引在 possible_keys 中出现, 但是并不表示此索引会真正地被 MySQL 使用到. MySQL 在查询时具体使用了哪些索引, 由 key 字段决定.
Key实际索引:
key列显示MySQL实际决定使用的键(索引)
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
key_len字节数:
表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,
即key_len是根据表定义计算而得,不是通过表内检索出的)
表示查询优化器使用了索引的字节数. 这个字段可以评估组合索引是否完全被使用, 或只有最左部分字段被使用到.
不损失精确性的情况下,长度越短越好
ref辅助列:
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
Extra额外信息:
该列包含MySQL解决查询的详细信息,有以下几种情况:
Using where:
列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候,
表示mysql服务器将在存储引擎检索行后再进行过滤
Using index
"覆盖索引扫描", 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错
Using temporary:
查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不高, 建议优化.
Using filesort:
MySQL中无法利用索引完成的排序操作称为“文件排序”,基于文件使用QuickSort。
优化:
对order by字段建立索引(普通or联合索引的最左原则),利用叶子节点存储主键的原理,先利用索引树进行排序,返回排序后的主键,再继续回表操作,快速找到对应的记录。
Using join buffer:
改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,
根据查询的具体情况可能需要添加索引来改进能。
Using index condition:
首先根据索引ref找到范围,然后利用索引下推进行过滤。(不是联合索引的话,只能Using where。注意count和select *计划不同,不用回表)
Impossible where:这个值强调了where语句会导致没有符合条件的行。
Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
常用函数:
日期相关:
CONVERT_TZ(FROM_UNIXTIME(update_time/1000,'%Y-%m-%d %H:%i:%S'),@@session.time_zone,"+07:00")
或手动添加时间差from_unixtime(floor((ts+tz*3600000)/1000))
unix_timestamp('2018-01-15 09:45:16')
字符串时间转时间戳
from_unixtime(1515980716, '%Y-%m-%d %H:%i:%S')
时间戳转str date
date_format('2018-01-15 09:45:16', '%Y-%m-%d')
时间格式化
date_add(str_date, interval 1 day) 支持hour、minute、second、microsecond、week、month、quarter、year
添加时间
date_sub()
减少时间,等于date_add的-x
DATEDIFF ( date1, date2 )
两个str date之间的时间间隔
条件判断:
is not null:
sql语句使用where xxx and xx is not null时,后半条件不起作用,需xx<>''即可
CASE WHEN:
用法1:
CASE field WHEN value1 THEN result1 WHEN value2 THEN result2 ELSE result3 END
将每行该field的值与各种value比较,得出result
用法二:
CASE WHEN field=value1 THEN result WHEN field=value2 THEN result2 ELSE result3 END
将每行该field的值导入表达式,得出result返回
IFNULL(expr1,expr2)是否为空:
假如expr1 不为 NULL,则 IFNULL() 的返回值为 expr1; 否则其返回值为 expr2。
mysql架构:
连接池JDBC:
服务层:
mysql连接池,管理服务、sql接口、解析器、优化器、缓存,这一层组件是可插拔的,可自定义。
中间层是MySQL的核心,包括查询解析、分析、优化和缓存等。同时它还提供跨存储引擎的功能,包括 存储过程、触发器和视图等。
引擎层:
存储引擎innodb,存储引擎层,它负责存取数据。服务器通过API可以和各种存储引擎进行交互。不同的存储引擎具有不同 的功能,我们可以根据实际需求选择使用对应的存储引擎
底层存储:
存储引擎:
存储引擎就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方 法。就像汽车的发动机一样, 存储引擎好坏 决定的数据库提供的功能和性能
指定:
create table(...) engine=MyISAM;
MyISAM 和 InnoDB 的区别?
1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
3. InnoDB 是聚簇索引,MyISAM 是非聚簇索引。
聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。
而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。 MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
聚集索引:該索引中鍵值的邏輯順序決定了表中相應行的物理順序
非聚集索引:該索引中索引的邏輯順序與磁碟上行的物理儲存順序不同
4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
场景:
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
以读为主的业务,例如:图片信息数据库,博客数据库,商品库等业务。
原因: (1)做很多count 的计算;(2)插入不频繁,查询非常频繁(MYISAM 不支持事务,也是它查询快的一个原因!);(3)没有事务。
INNODB 在做 SELECT 的时候,要维护的东西比 MYISAM 引擎多很多。比如mvcc多版本并发控制,
innodb寻址要映射到块,再到行,而mysaim非聚蔟索引直接记录的是文件的offset,定位快。
缓存的东西不一样,mysaim只缓存索引即可。
写入操作,innodb需要维护表级缓存,mvcc,外键,事务锁,而mysaim只维护索引。
对数据一致性要求不是非常高的业务(不支持事务)
硬件资源比较差的机器可以用 MyiSAM (占用资源少)
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
sql查询流程:
1. 通过客户端/服务器通信协议与 MySQL 建立连接
2. 查询缓存,这是 MySQL 的一个可优化查询的地方,如果开启了 Query Cache 且在查询缓存过程中查 询到完全相同的 SQL 语句,则将查询结果直接返回给客户端;如果没有开启Query Cache 或者没有查询到 完全相同的 SQL 语句则会由解析器进行语法语义解析,并生成解析树。
3. 预处理器生成新的解析树。 4. 查询优化器生成执行计划。
5. 查询执行引擎执行 SQL 语句,此时查询执行引擎会根据 SQL 语句中表的存储引擎类型,以及对应的 API 接口与底层存储引擎缓存或者物理文件的交互情况,得到查询结果,由MySQL Server 过滤后将查询结 果缓存并返回给客户端。若开启了 Query Cache,这时也会将SQL 语句和结果完整地保存到 Query Cache 中,以后若有相同的 SQL 语句执行则直接返回结果。
mysql物理文件:
物理文件包括:日志文件,数据文件,配置文件
日志文件
error log 错误日志 排错 /var/log/mysqld.log【默认开启】 bin log 二进制日志 备份 增量备份 DDL DML DCL
Relay log 中继日志 复制 接收 replication master
slow log 慢查询日志 调优 查询时间超过指定值
数据文件
1、.frm文件 不论是什么存储引擎,每一个表都会有一个以表名命名的.frm文件,与表相关的元数据(meta)信息都存放在 此文件中,包括表结构的定义信息等。
2、.MYD文件 myisam存储引擎专用,存放myisam表的数据(data)。每一个myisam表都会有一个.MYD文件与之呼应,同 样存放在所属数据库的目录下
3、.MYI文件 也是myisam存储引擎专用,存放myisam表的索引相关信息。每一个myisam表对应一个.MYI文件,其存放的 位置和.frm及.MYD一样
4、.ibd文件 存放innoDB的数据文件(包括索引)。
5. db.opt文件 此文件在每一个自建的库里都会有,记录这个库的默认使用的字符集和校验规。
慢日志查询:
介绍:
MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间 超过阈值的语句。
认情况下,MySQL数据库并不启动慢查询日志,需要手动来设置这个参数。 如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。
慢查询日志支持将日志记录写入文件和数据库表。
参数:
SHOW VARIABLES LIKE "%query%" ;
slow_query_log:是否开启慢查询日志, 表示开启, 表示关闭。
SHOW VARIABLES LIKE "%query%" ;
slow-query-log-file:新版(5.6及以上版本)MySQL数据库慢查询日志存储路径。
long_query_time: 慢查询阈值,当查询时间多于设定的阈值时,记录日志。
启用:
set global slow_query_log=1;
如果要永久生效,就必须修改配置文件my.cnf
set global long_query_time=1;阈值时间
log_output='FILE,TABLE'。日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资 源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件.
set global log_queries_not_using_indexes=1;使用索引的查询也被记录到慢查询日志中(可选 项)。如果调优的话,建议开启这个选项。
测试:
可以看到有时间戳,用户,查询时长及具体的SQL等
mysql集群:
主从复制:
概述:
三个线程:主的io线程,从的io线程和sql线程
复制流程:
1.从库连接主库,要求发送bin_log文件,并发送自带的读取位置
Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
2.主库把数据发送给从库(从库的主动pull)的relay-log
Master接收到来自Slave的IO进程的请求后,通过负责复制的IO进程(log dump线程)根据请求信息读取制定日志指定位置之后的日志信息,
返回给Slave的IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置;
3.Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,
并将读取到的Master端的 bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log的哪个位置
开始往后的日志内容,请发给我”;
4.Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行。
配置:
从库设置readonly
主从同步备份可以指定特定库
复制延迟问题解决:
1.从库使用ssd硬盘。
2.尽量避免主库大量的写入 ,消息队列写入
3.主从库之间使用专用网络
4.对于数据一致性要求高的,查主库
5.使用多个从库减少压力
延迟复制作用:
1.数据库误操作后,快速从库恢复数据。
2.延迟测试
3.老数据查询
命令:
stop slave;
change master to master_delay = 600;
start slave
半同步复制:
主库等待从库也同步成功时返回确认,高延迟时降级改为异步复制
主从不一致问题:
1.忽略错误,继续同步。
set global sql_slave_skip_counter =1;
2.清空从库,从库从0开始同步主库(不停主库 不锁表)
3.清空从库,主库dump导入从库,start slave,完全开始同步,如风控midatadb为2t,需要一天时间(不停主库 不锁表)
# 其实不需从0开始同步,花费时间更长
①dump主库数据到alldatas.sql
如果想要备份指定库,需要添加 replicate_wild_do_table 选项,为了保持数据的完全一致性,个人不建议只备份指定库。
全部库dump的话,下面的重新指定从库bin log offset才可以改写
②将从库上的数据库清空,并还原为普通的数据库,(删除 master.info relay-log.info relay-bin.index )
③重置 MySQL 数据库的 master 和 slave ,重置 slave 时,先停止 slave(stop slave;)
reset master;
show master status\G
stop slave;
reset slave;
show slave status\G
④接下来就是 导入数据库信息,导入之后 重新指向主库:
注意:重新指向主库的 master_log_file 和 master_log_pos,不是主库 show master status; 显示的信息,而是从主库上备份的文件里的信息
vim alldatas.sql 里面包含dump时主库的offset
mysql -uroot -p < alldatas.sql
change master to master_host='192.168.1.1', master_user='Skon',master_password='Skon123',master_log_file='binlog.000204',
master_log_pos=547507087;
start slave;
show slave status\G
4.只清空冲突表(不停主库 不锁表)
先stop slaves停掉主从,将破坏的库drop掉,然后停一下主库
# 不停应该也可以,单事务dump,但可能导致不一致,如先update再delete,这时候可以看备份的日志log,然后SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
# 跳过当前因为dump从库更新了提前offset导致的一处错误就可以了
将主库的对应表dump出来,导入到从库,start slave,然后接着同步,主从之间其他表只是延迟了,破坏库变为一致了。
--single-transaction
binlog:
作用:
1.用于主从复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
2.用于数据库的基于时间点的还原。
内容:
逻辑格式的日志。
记录的是执行过的事务中的sql语句和包括了执行的sql语句(增删改)反向的信息,比如:delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。
产生时机(事务提交):
事务提交的时候一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。
这里与redo log很明显的差异就是redo log并不一定是在事务提交的时候刷新到磁盘,redo log是在事务开始之后就开始逐步写入磁盘。开启了bin_log的情况下,对于较大事务的提交,可能会变得比较慢一些。
MySQL通过两阶段提交过程来保证事务的一致性的,也即redo log和binlog的一致性,理论上是先写redo log,再写binlog,两个日志都提交成功(刷入磁盘),事务才算真正的完成。
过期配置:
binlog的默认是保持时间由参数expire_logs_days配置,对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后会被自动删除。
与redo log的区别:
1,作用不同:redo log是保证事务的持久性的,是事务层面的,binlog作为还原的功能,是数据库层面的(当然也可以精确到事务层面的),虽然都有还原的意思,但是其保护数据的层次是不一样的。
2,内容不同:redo log是物理日志,binlog是逻辑日志。这也导致恢复数据时候的效率不同,redo log效率高于binlog。
3,两者日志产生的时间,可以释放的时间,在可释放的情况下清理机制,都是完全不同的。
支持格式:
Row:
概述:
日志中会记录成每一行数据被修改的形式,然后在 slave 端再对相同的数据进行修改。
优点:在 row 模式下,bin-log 中可以不记录执行的 SQL 语句的上下文相关的信息,仅仅只需要记录那一条记录被修改了,修改成什么样了。
所以 row 的日志内容会非常清楚的记录下每一行数据修改的细节,非常容易理解。而且不会出现某些特定情况下的存储过程或 function ,
以及 trigger 的调用和触发无法被正确复制的问题。
任何情况都可以被复制,这对复制来说是最安全可靠的;多数情况下,从服务器上的表如果有主键的话,复制就会快了很多;
从服务器上采用多线程来执行复制成为可能;
缺点:
在 row 模式下,所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容,
比如有这样一条 update 语句UPDATE product SET owner_member_id = 'b' WHERE owner_member_id = 'a'
执行之后,日志中记录的不是这条 update 语句所对应的事件 (MySQL 以事件的形式来记录 bin-log 日志) ,而是这条语句所更新
的每一条记录的变化情况,这样就记录成很多条记录被更新的很多个事件。自然,bin-log 日志的量就会很大。
尤其是当执行 alter table 之类的语句的时候,产生的日志量是惊人的。
不能从 binlog 中看到都复制了写什么语句(加密过的);
Statement:
概述:
默认格式
每一条会修改数据的 SQL 都会记录到 master 的 bin-log 中,slave在复制的时候 SQL 进程会解析成和原来 master 端执行过的相同的 SQL 再次执行
优点:在 statement 模式下,首先就是解决了 row 模式的缺点,不需要记录每一行数据的变化,减少了 bin-log 日志量,节省 I/O 以及存储资源,
提高性能。因为他只需要记录在 master 上所执行的语句的细节,以及执行语句时候的上下文的信息。
# 还能根据这个sql语句用canal来发binlog到kafka上面
binlog 中包含了所有数据库修改信息,可以据此来审核数据库的安全等情况;
binlog 可以用于实时的还原,而不仅仅用于复制;
主从版本可以不一样,从服务器版本可以比主服务器版本高;
缺点:
在 statement 模式下,由于他是记录的执行语句,所以,为了让这些语句在 slave 端也能正确执行,
那么他还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,以保证所有语句在 slave 端杯执行的时候能够得到
和在 master 端执行时候相同的结果。另外就是,由于 MySQL 现在发展比较快,很多的新功能不断的加入,使 MySQL 的复制遇到了不小的挑战,
自然复制的时候涉及到越复杂的内容,bug 也就越容易出现。
在 statement 中,目前已经发现的就有不少情况会造成 MySQL 的复制出现问题,主要是修改数据的时候使用了某些特定的函数或者功能的时候
会出现,比如:sleep() 函数在有些版本中就不能被正确复制,在存储过程中使用了 last_insert_id() 函数,
可能会使 slave 和 master 上得到不一致的 id 等等。由于 row 是基于每一行来记录的变化,所以不会出现类似的问题。
复制须要执行全表扫描 (WHERE 语句中没有运用到索引) 的 UPDATE 时,须要比 row 请求更多的行级锁;
不是所有的 UPDATE 语句都能被复制,尤其是包含不确定操作的时候;
调用具有不确定因素的 UDF 时复制也可能出现问题;
一些函数如LOAD_FILE()、UUID()、FOUND_ROWS()、USER()等不能复制(mixed会检测这个并切换到row格式)
对于有 AUTO_INCREMENT 字段的 InnoDB 表而言,INSERT 语句会阻塞其他 INSERT 语句;
Mixed:
概述:
从 5.1.8 版本开始,MySQL 提供了除 Statement 和 Row 之外的第三种复制模式:Mixed,实际上就是前两种模式的结合。
在 Mixed 模式下,MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 statement 和 row 之间选择一种。
新版本中的 statment 还是和以前一样,仅仅记录执行的语句。
而新版本的 MySQL 中对 row 模式也被做了优化,并不是所有的修改都会以 row 模式来记录,比如遇到表结构变更的时候就会以 statement 模式来记录,
如果 SQL 语句确实就是 update 或者 delete 等修改数据的语句,那么还是会记录所有行的变更。
缺点:
有可能出现复制问题,或者性能问题。还不如直接指定。
基于canal发送binlog:
1).canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
2).MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
3).canal 解析 binary log 对象(原始为 byte 流)
安装:
1.安装服务器
2.配置Master主服务器
a.在Master MySQL上创建一个用户‘repl’,并允许其他Slave服务器可以通过远程访问Master,通过该用户读取二进制日志,实现数据同步。
- mysql>create user repl; //创建新用户
# repl用户必须具有REPLICATION SLAVE权限,除此之外没有必要添加不必要的权限,密码为mysql。
说明一下192.168.0.%,这个配置是指明repl用户所在服务器,
这里%是通配符,表示192.168.0.0-192.168.0.255的Server都可以以repl用户登陆主服务器。
当然你也可以指定固定Ip。
- mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.0.%' IDENTIFIED BY 'mysql';
b.找到MySQL安装文件夹修改my.Ini文件。mysql中有好几种日志方式,这不是今天的重点。
我们只要启动二进制日志log-bin就ok。
在[mysqld]下面增加下面几行代码
server-id=1 //给数据库服务的唯一标识,一般为大家设置服务器Ip的末尾号
log-bin=master-bin # 如果互为主从的话,从服务器也要开启
log-bin-index=master-bin.index
c.查看日志
SHOW MASTER STATUS;
重启MySQL服务
3.配置Slave从服务器(windows)
a.找到MySQL安装文件夹修改my.ini文件,在[mysqld]下面增加下面几行代码
[mysqld]
server-id=2 # server-id 一定要和主库的不同
relay-log-index=slave-relay-bin.index
relay-log=slave-relay-bin
重启MySQL服务
b.连接Master
change master to master_host='192.168.0.104', //Master 服务器Ip
master_port=3306,
master_user='repl',
master_password='mysql',
master_log_file='master-bin.000001',//Master服务器产生的日志
master_log_pos=0;
c.启动Slave
start slave; # 停止主从同步stop slave
4、Slave从服务器(Ubuntu)
a. 找到MySQL安装文件夹修改my.cnf文件,vim my.cnf
basedir = /usr/local/mysql
datadir = /usr/local/mysql/data
port = 3306
server_id = 3
relay-log-index = slave-relay-bin.index
relay-log=slave-relay-bin
b.重启MySQL服务
/support-files/myql.server restart
./bin/mysql 进入MySQL命令窗口
c.连接Master
change master to master_host='192.168.0.104', //Master 服务器Ip
master_port=3306,
master_user='repl',
master_password='mysql',
master_log_file='master-bin.000001',//Master服务器产生的日志
master_log_pos=0;
d.启动Slave
start slave;
分库分表方案:
概述:
单表数据条多于1000w时性能也会下降,这时就得用DAL了
1.客户端分片
根据用户的uid分库分表
2.数据层中间件
mysql-proxy 自动判断读写而分配,但性能低,不如客户端分片简单,还得搭架构(负载均衡+高可用)
atlas
基于mysql-proxy的c语言二次开发,重写网络模型和线程模型,连接池,锁
有分表功能,但局限性
cobar
阿里巴巴开源
mycat
基于cobar的二次开发
3.mysql集群
mysql cluster
PXC,三个节点数据相互同步
数据高可用方案:
1.一主一从,高可用(双机高可用方案)
概述:
一台机器A作为读写库,另一台B作为备份库;A库故障后B库作为读写库;A库恢复后A作为备库。
适合场景:
低读低写并发、低数据量方案,读和写都不高的场景(单表数据低于500万)。
开发说明:
此种情况下,数据源配置中的数据库IP地址,可采用虚拟的IP地址。
虚拟IP地址由两台数据库机器上的keepalive配置,并互相检测心跳。
当其中一台故障后,虚拟IP地址会自动漂移到另外一台正常的库上。
数据库的主备配置、故障排除和数据补全,需要DBA和运维人员来维护。而程序代码或配置并不需要修改。
优点:
一个机器故障了可以自动切换;
缺点:
只有一个库在工作,读写并未分离,并发有限制。
2.一主一从,读写分离
概述:
一台机器A作为写库,另一台B作为读库;A库故障后B库充当读写,A修复后,B库为写库,A库为读库。
在客户端读写分离,要注意主从延时情况
适合场景:
主从结构方案,读和写都不是非常高的场景(单表数据低于1000万),高可用。比方案一并发要高很多。
优化:
在写比较多的表上可以在master不建立索引,而在slave端来建立索引,一个表有索引插入会慢
开发说明:
这种方案的实现,要借助数据库中间件Mycat来实现,Mycat的datahost配置如下(注意balance和writetype的设置)
项目开发中,要配置Mycat数据源,并实现对Mycat数据源的数据操作。数据库A和数据库B应该互为主从。
数据库的主主配置、故障排除和数据补全,依然需要DBA和运维人员来维护。
优点:
一个机器故障了可以自动切换;
缺点:
引入了一个Mycat节点,若要高可用需要引入至少两个Mycat。
常规的解决方案是引入haproxy和keepalive对mycat做集群。
3.一主多从,读写分离
概述:
一个主写库A多个从库,当主库A故障时,提升从库B为主写库,同时修改C、D库为B的从库。A故障修复后,作为B的从库。
开发说明:
项目开发中需要使用Mycat作为中间件,来配置主库和从库。
主库A故障后,Mycat会自动把从B提升为写库。
而C、D从库,则可以通过MHA等工具,自动修改其主库为B。进而实现自动切换的目地。
MHA功能:
MHA Manager可以单独部署在一台独立的机器上管理多个master-slave集群,也可以部署在一台slave节点上。
MHA Node运行在每台MySQL服务器上,MHA Manager会定时探测集群中的master节点,
当master出现故障时,它可以自动将最新数据的slave提升为新的master,然后将所有其他的slave重新指向新的master。
整个故障转移过程对应用程序完全透明。
优缺点:
由于配置了多个读节点,读并发的能力有了质的提高。理论上来说,读节点可以多个,可以负载很高级别的读并发。
当然,Mycat依然需要设计高可用方案。
适合场景:
高读低写并发、低数据量方案,该架构适合写并发不大、但是读并发大的很的场景
优化:
在客户端实现多个从节点的轮询和权重的设置(比负载均衡容易实现)
4.多主0从
概述:
多个数据库,在负载均衡作用下,可同时进行写入和读取操作;
各个库之间以Galera Replication的方法进行数据同步,即每个库理论上来说,数据是完全一致的。
开发说明:
数据库读写时,只需要修改数据库读写IP为keepalive的虚拟节点即可;
数据库配置方面相对比较复杂,需要引入haproxy、keepalive、Galaera等各种插件和配置。
适合场景:
高读写并发、低数据量方案,适合读写并发较大、数据量不是非常大的场景。
优点:
1)可以在任意节点上进行读
2)自动剔除故障节点
3)自动加入新节点
4)真正并行的复制,基于行级
5)客户端连接跟操作单数据库的体验一致。
6) 同步复制,因此具有较高的性能和可靠性。
缺点:
1) DELETE操作不支持没有主键的表,没有主键的表在不同的节点顺序将不同
2)处理事务时,会运行一个协调认证程序来保证事务的全局一致性,
若该事务长时间运行,就会锁死节点中所有的相关表,导致插入卡住(这种情况和单表插入是一样的)。
3)整个集群的写入吞吐量是由最弱的节点限制,如果有一个节点变得缓慢,那么整个集群将是缓慢的。
为了稳定的高性能要求,所有的节点应使用统一的硬件
4)如果DDL语句有问题将破坏集群,建议禁用。
5) Mysql数据库5.7.6及之后的版本才支持此种方案。
5.多主多从
概述:
采用Mycat进行分片存储,可以解决写负载均衡和数据量过大问题;每个分片配置多个读从库,可以减少单个库的读压力。
适合场景:
高读写并发、高数据量方案,读写并发都很大并且数据量非常大的场景。
开发说明:
需要配置Haproxy、keepalive和mycat集群,每个分片上又需要配置一主多从的集群。
每个分片上的完整配置,具体请参考方案三,可以简单地把方案三理解为一个分片结构。
因此,配置和维护量都比较大。
优点:
终极的解决高并发高数据量的方法。
缺点:
配置和维护都比较麻烦,需要的软硬件设备资源大。
生产优化经验:
1.避免使用select *
原因:
性能是差不多的,但带来了额外的网络开销。
而指定列可以清楚返回了哪些字段,可读性更强。特定情况下可以用到覆盖索引。
2.固定字段长度放前放,避免取后面的列时不能直接跳过固定长度
char固定/varchar不固定,创建表时尽量时 char 代替 varchar(避免更新导致碎片化无法重复利用问题)
3.count计数
count(*)在统计结果的时候,不会忽略列值为NULL
默认情况下推荐
count(1)在统计结果的时候,不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空的行。速度与该列的类型有关。
5.多进程连接mysql
当一定数量(>1)的进程(无论process还是进程池)获取的mysql数据量比较小时,就不会出现返回null
否则,较大量后,可能返回为[]
解决:
1.每次获取小量数据
2.设置time.sleep
3.使用多线程,因为多进程是并行,而多线程是并发,遇到IO切换
可能原因:
多进程并行
6.随机select:
1.select * from users order by rand() LIMIT 1
MYSQL手册里面针对RAND()的提示大概意思就是,在 ORDER BY从句里面不能使用RAND()函数,因为这样会导致数据列被多次扫描,
导致效率相当相当的低,效率不行,切忌使用。
数据量大的时候,效率较低
2.SELECT * FROM users
WHERE userId >= ((SELECT MAX(userId) FROM users)-(SELECT MIN(userId) FROM users)) * RAND() + (SELECT MIN(userId) FROM users) LIMIT 1
结果随机,速度较快
缺点是对主键有限制,不能跳跃过大
常见问题:
百万级别或以上的数据如何删除?
关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。
所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
1.所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
2.然后删除其中无用数据(此过程需要不到两分钟)
3.删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
4.与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
其他做法:
逐步删除chunks数据
如何删大容量的表?
独立表空间和共享表空间
共享表空间:某一个数据库的所有的表数据,索引文件全部放在一个文件中,默认这个共享表空间的文件路径在data目录下。
默认的文件名为:ibdata1(此文件,可以扩展成多个)。注意,在这种方式下,运维超级不方便。你看,所有数据都在一个文件里,要对单表维护,十分不方便。
另外,你在做delete操作的时候,文件内会留下很多间隙,ibdata1文件不会自动收缩。换句话说,使用共享表空间来存储数据,会遭遇drop table之后,空间无法释放的问题。
独立表空间:每一个表都以独立方式来部署,每个表都有一个.frm表描述文件,还有一个.ibd文件。
.frm文件:保存了每个表的元数据,包括表结构的定义等,该文件与数据库引擎无关。
.ibd文件:保存了每个表的数据和索引的文
删除大文件:
ibd文件利用了linux中硬链接的知识,来进行快速删除。
system ln /data/mysql/mytest/erp.ibd /data/mysql/mytest/erp.ibd.hdlk
然后drop table erp;
最后删除erp.ibd.hdlk
在生产环境,直接用rm命令来删大文件,会造成磁盘IO开销飙升,CPU负载过高,是会影响其他程序运行的。
应该用truncate命令来删
TRUNCATE=/usr/local/bin/truncate
for i in `seq 2194 -10 10 `;
do
sleep 2
$TRUNCATE -s ${i}G /data/mysql/mytest/erp.ibd.hdlk
done
rm -rf /data/mysql/mytest/erp.ibd.hdlk ;
超大分页问题;
性能分析:
select * from user limit 1000000,10 mysql查找到前10010条数据,之后丢弃前面的10000行,这个步骤其实是浪费掉的.
优化:
1.用id优化
先找到上次分页的最大ID,然后利用id上的索引来查询,类似于select * from user where id>1000000 limit 100.
可以记录上次查询的最大ID,下次查询时直接根据该ID来查询
这样的效率非常快,因为主键上是有索引的,但是这样有个缺点,就是ID必须是连续的,并且查询不能有where语句,因为where语句会造成过滤数据.
2.用覆盖索引优化
mysql的查询完全命中索引的时候,称为覆盖索引,是非常快的,因为查询只需要在索引上进行查找,之后可以直接返回,而不用再回数据表拿数据(offset limit的话是会拿数据的).因此我们可以先查出索引的ID,然后根据Id拿数据.
先快速定位需要获取的id段,然后再关联
select * from (select id from job limit 1000000,100) a left join job b on a.id = b.id;
SELECT * FROM tbl_works t1 JOIN (SELECT id from tbl_works WHERE status=1 limit 100000, 10) t2 ON t1.id = t2.id
是否有where不影响?
有where走覆盖索引,没有where的话也能避免取出来数据再丢弃的IO,直接取出索引,然后再取字段。
主键使用自增ID还是UUID?
与mysql底层原理的契合:
因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),
如果主键索引是自增ID,那么只需要不断向后排列即可,
如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。
innodb 中的主键是聚簇索引,会把相邻主键的数据安放在相邻的物理存储上。如果主键不是自增,而是随机的,那么频繁的插入会使 innodb 频繁地移动磁盘块,而影响写入性能。
自增主键
场景:
只推荐在非核心业务表
自增主键用完了怎么办?
1.把自增主键的类型改为BigInt类型
在线修改,导致这张表无法进行更新类操作(DELETE、UPDATE、DELETE)。
借助第三方工具,会创建新表,触发器记录数据修改,拷贝数据,rename,删除触发器。如果表里有触发器和外键,不可行。
改从库表结构,然后主从切换。直接在从库上进行表结构修改,不会阻塞从库的读操作。改完之后,进行主从切换即可。
2.int类型范围为0~2147483648,量多的情况下,分库分表。
缺点:
自增存在回溯问题
自增值在服务器端产生(加AI锁),存在并发性能问题。
自增值做主键,只能在当前实例中保证唯一性,不能保证分布式全局唯一。
公开数据值,容易引发安全问题
UUID
设计:
将时间高位放在最前,避免插入时乱序问题,使用函数UUID_TO_BIN("uuid",TRUE),mysql8.0前可以自定义函数
PK=时间字段+随机码+业务信息1+业务信息2,例如订单号
优点:
UUID能保证全局唯一,数据合并方便
主键为什么不推荐有业务含义?
(1)因为任何有业务含义的列都有改变的可能性,主键一旦带上了业务含义,那么主键就有可能发生变更。主键一旦发生变更,该数据在磁盘上的存储位置就会发生变更,有可能会引发页分裂,产生空间碎片。
(2)带有业务含义的主键,不一定是顺序自增的。那么就会导致数据的插入顺序,并不能保证后面插入数据的主键一定比前面的数据大。如果出现了,后面插入数据的主键比前面的小,就有可能引发页分裂,产生空间碎片。
为什么不直接存储图片、音频、视频等大容量内容?
(1)Mysql内存临时表不支持TEXT、BLOB这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。导致查询效率缓慢
(2)binlog内容太多。因为你数据内容比较大,就会造成binlog内容比较多。大家也知道,主从同步是靠binlog进行同步,binlog太大了,就会导致主从同步效率问题!
字段为什么要定义为NOT NULL?
(1)索引性能不好
Mysql难以优化引用可空列查询,它会使索引、索引统计和值更加复杂。可空列需要更多的存储空间,还需要mysql内部进行特殊处理。可空列被索引后,每条记录都需要一个额外的字节,还能导致MYisam 中固定大小的索引变成可变大小的索引。
(2)查询会出现一些不可预料的结果
select count(name) from table_2;会忽略空列。因为null列的存在,会出现很多出人意料的结果,从而浪费开发时间去排查Bug.
如何存储sex等有限字段?
tinyint允许了脏数据
enum在8.0之前可以用,但错误提示不明显
8.0之后提供char约束功能,如:
sex char(1) collate utf8mb4_general_ci default null,
constraint `user_chk_1` CHECK(((`sex`=_utf8mb4'M')or(`sex`=_utf8mb4'F')))
密码如何存储?
普通做法:md5破解,可以暴力枚举
升阶做法:加盐。(但可能被离职员工暴露,相同密码的结果一样;固定使用md5算法如果md5本身被破解了影响很大)
正确做法:动态盐+非固定加密算法
$salt$algorithm$value
示例:
$zpfsas$v1$sdasdfasdasd
大数据量的表怎么优化?
1.限定数据的范围,务必禁止不带任何限制数据范围条件的查询语句。
2.读/写分离,经典的数据库拆分方案,主库负责写,从库负责读;
3.缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存;
4.分库分表,主要有垂直分表和水平分表
第一阶段 优化sql和索引
(1)用慢查询日志定位执行效率低的SQL语句
(2)用explain分析SQL的执行计划
(3)确定问题,采取相应的优化措施,建立索引啊,等
第二阶段 搭建缓存
在优化sql无法解决问题的情况下,才考虑搭建缓存。毕竟你使用缓存的目的,就是将复杂的、耗时的、不常变的执行结果缓存起来,降低数据库的资源消耗。
注意,强一致性不能用缓存。
第三阶段 读写分离
(1)主从的好处?
回答:实现数据库备份,实现数据库负载均衡,提交数据库可用性。
读写分离(读库mysaim)、主从复制,双主复制(两台服务器互为主从,任何一台服务器数据变更,都会通过复制应用到另外一方的数据库中。双主单写。常见方案为MMM、MHA)
(2)主从的原理?
从库有两个线程,一个I/O线程,一个SQL线程,I/O线程读取主库传过来的binlog内容并写入到relay log,SQL线程从relay log里面读取内容,写入从库的数据库。
(3)如何解决主从一致性?
根据CAP定理,主从架构本来就是一种高可用架构,是无法满足一致性的
利用缓存,来解决该问题(先查缓存)。
步骤如下:
1、自己通过测试,计算主从延迟时间,建议mysql版本为5.7以后,因为mysql自5.7开始,多线程复制功能比较完善,一般能保证延迟在1s内。不过话说回来,mysql现在都出到8.x了,还有人用5.x的版本么。
2、数据库的写操作,先写数据库,再写cache,但是有效期很短,就比主从延时的时间稍微长一点。
3、读请求的时候,先读缓存,缓存不存在(这时主从同步已经完成),再读数据库。
第四阶段 利用分区表
第五阶段 垂直拆分
垂直拆分的复杂度还是比水平拆分小的。将你的表,按模块拆分为不同的小表。
拆分原则一般是如下三点:
(1)把不常用的字段单独放在一张表。
(2)把常用的字段单独放一张表
(3)经常组合查询的列放在一张表中(联合索引)。
第六阶段 水平拆分
数据库和缓存双写一致性方案:
种类:
1.先更新数据库,再更新缓存
2.先删除缓存,再更新数据库
3.先更新数据库,再删除缓存
先更新数据库,再更新缓存:
普遍反对
原因:
1.因为网络等原因,B却比A更早更新了缓存。导致脏数据。
2.如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
先更缓存,再更新数据库:
同样缓存会被频繁的更新
先删缓存,再更新数据库:
有问题的场景:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
读写分离同样的场景:
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
解决:
采用延时双删策略,可以同步也可以异步。
先更新数据库,再删缓存(推荐):
有问题的场景:
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
这种场景概率比第二种方案出现的小很多。
如何解决:
给缓存设有效时间,删除旧数据
异步延时删除策略
同步延时删除策略失败了怎么办?
重试机制,发到消息队列。
异步删除方案:
1.更新完数据库后,业务线程将key发到消息队列
2.canal订阅程序读取binlog作为消息队列,然后触发。