mysql运维笔记

122 阅读12分钟

1.基础架构

1.1.一条查询sql语句的执行流程

image.png

1.2.常见msyql客户端命令

#客户端登录
mysql -h127.0.0.1 -p -P3310 -uroot

#查看版本
select version();

#显示数据库
show databases;

# 指定数据库,并显示表
 use edu-common;
 show tables;
 
#查询数据表
select * from a_test;

#查看客户端连接进程
show processlist;

#查看系统配置
show variables;
show variables like 'query%';

#常用参数
空闲连接超时:wait_timeout
查询缓存:query_cache_type

1.3.重要的日志模块

  • 重做日志:redo log

image.png

  • 两阶段提交

image.png

#重要的日志模块
##1.重做日志:redo log
-是InnoDB引擎特有,有crash-safe能力
-固定大小,比如配置一组4个文件,每个文件大小1G
-是物理日志,记录在数据页上做了什么修改
-循环写(相当于环形缓冲区),空间固定会用完

##2.归档日志:binlog
-是server层提供,所有存储引擎都可以使用
-是逻辑日志,记录sql语句的原始逻辑
-追加写,不会覆盖以前的日志

1.4.事务

#事务ACID原则
原子性、一致性、隔离性、持久性

#事务隔离级别
读未提交--->脏读
读已提交---> 通过一致性视图实现
可重复读--->通过一致性视图实现,所谓一致性视图,其实就是一个由事务id组成的数组
串行化--->加锁

#查看事务隔离级别
show variables like 'transaction_isolation%';

#关于mvcc
多版本并发控制,即一条记录在系统中存在多个版本,通过回滚段与事务Id实现

#关于长事务
在实际应用中,尽量不要使用长事务,原因:长事务会因为回滚段的存在而占用更多的内存资源,和锁资源

#关于事务的启动方式
begin/start transaction--->等到语句执行的时候,才真正开启事务
start transaction with consistency snapshot--->立即开启事务

#查看长事务
select * from information_schema.INNODB_TRX;
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

1.5.索引

#关于索引的理解
索引是利用空间换时间,提升查询数据的性能
索引有序

#索引数据模型
哈希--->哈希索引,适合精确匹配搜索,不能实现区间搜索
有序数组--->适合区间搜索,更新性能低
搜索树---> B+树,降低树的高度,减少操作磁盘IO的次数,提升性能
跳表--->
LSM树

#索引组织表
mysql中的InnoDB引擎,支持索引组织表,即一个表如果没有创建主键,会有默认的主键

#主键索引,二级索引,复合索引
主键索引存储数据页(一个数据页,默认16k大小,存储多个记录)
二级索引存储主键值,查询的时候,先根据索引获取到主键值,再回表查询获取记录数据
复合索引是有多个字段组成的索引,有索引覆盖(减少回表),前缀匹配等作用

#唯一索引,与普通索引
如果是写多读少,且在业务应用中保证了唯一性,推荐使用普通索引,可以利用上change buffer,降低操作磁盘次数,提升性能
如果是归档库,因为数据已经保证了唯一性,推荐使用普通索引

在实际业务中,当然在数据库层面借助唯一索引,保证唯一性更好

#查看执行计划
explain select * from a_test;
explain select * from a_test where name like 'xiao%';

#重建索引,删除索引
alter table a_test add index(name);
alter table a_test drop index name;

#分析表
analyze table a_test;

#索引hint
explain select * from a_test use index(name) where name like 'xiao%';
explain select * from a_test force index(name) where name like 'xiao%';

#mysql优化器,选择索引的参考因子
基数--->采样统计获取,选择N个数据页,当数据更新变化有1/M的时候,会更新统计信息;或者执行analyze table t;
排序--->
是否使用临时表--->

#选错索引

--建表
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `b` (`b`)
) ENGINE=INNODB;

--初始化数据
delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=100000)do
    insert into t values(i, i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();

--分析sql
mysql> select * from t where a between 10000 and 20000;

--强制走索引
set long_query_time=0;
select * from t where a between 10000 and 20000; /*Q1*/
select * from t force index(a) where a between 10000 and 20000;/*Q2*/

#字符串建立索引,参考案例:区分度是关键
mysql> select count(distinct email) as L from SUser;


mysql> select 
  count(distinct left(email,4))as L4,
  count(distinct left(email,5))as L5,
  count(distinct left(email,6))as L6,
  count(distinct left(email,7))as L7,
from SUser;

假设区分度损失可接受:5%,那么统计出来的值:L4/L5/L6/L7,只要大于:L * 95%即可

1.6.锁

#全局锁
锁整个实例,让整个实例只读
flush table with read lock;

set global readonly=true;

unlock tables;

#表锁
lock tables a_test read/write;
unlock tables;

#MDL锁
它也是表级锁,不需要显示处理
如果是DML操作,系统会自动加上MDL读锁
如果是DDL操作,系统会自动加上MDL写锁

#行锁
行锁,即锁的粒度在行
行锁的两阶段锁协议,开始操作加锁,事务结束后才会释放锁
行锁死锁解决办法:超时,或者死锁检测

#查看行锁超时时间,以及死锁检测参数
show variables like 'innodb_lock_wait%';
show variables like 'innodb_deadlock_detect%';

#查看被锁的表
select * from INFORMATION_SCHEMA.INNODB_TRX;
#1、查询进程
show full processlist 查询到相对应的进程===然后 kill id

#2、查询是否锁表(查看正在被锁定的的表)
show OPEN TABLES where In_use > 0;

show status like '%lock%'

#更新逻辑走当前读,查询逻辑如果加锁走当前读
mysql> select k from t where id=1 lock in share mode;--->读锁,S锁,共享锁
mysql> select k from t where id=1 for update;--->写锁,X锁,排他锁

1.7.备份还原操作

#直接备份,指定备份文件位置
mysqldump -h127.0.0.1 -uroot -padmin -P3310 edu-common >D:\03other\02study\edu-common20200906.sql

# 删除表格式备份(能够让该备份覆盖已有数据库而不需要手动删除原有数据库)
mysqldump --add-drop-table -h127.0.0.1 -uroot -padmin -P3310 edu-common >D:\03other\02study\edu-common20200906_1.sql

#压缩备份(linux)
mysqldump -h127.0.0.1 -uroot -padmin -P3310 edu-common |gzip >D:\03other\02study\edu-common20200906.sql.gz

#指定表备份
mysqldump -h127.0.0.1 -uroot -padmin -P3310 edu-common a_test >D:\03other\02study\edu-common20200906_2.sql

#仅备份数据库结构,不备份数据
mysqldump -h127.0.0.1 -uroot -padmin -P3310 --no-data --databases edu-common >D:\03other\02study\edu-common20200906_3.sql

----------------------------------------------------------
#还原mysql数据库
mysql -h127.0.0.1 -uroot -padmin -P3310 edu-common < D:\03other\02study\edu-common20200906_3.sql

#还原压缩数据库
gunzip < backupfile.sql.gz | mysql -h127.0.0.1 -uroot -ppass myweb

全量备份参考脚本

#!/bin/bash
# mysql 数据库全量备份

# 用户名、密码、数据库名
username="root"
password="tencns152"
dbName="goodthing"

beginTime=`date +"%Y年%m月%d日 %H:%M:%S"`
# 备份目录
bakDir=/home/mysql/backup
# 日志文件
logFile=/home/mysql/backup/bak.log
# 备份文件
nowDate=`date +%Y%m%d`
dumpFile="${dbName}_${nowDate}.sql"
gzDumpFile="${dbName}_${nowDate}.sql.tgz"

cd $bakDir
# 全量备份(对所有数据库备份,除了数据库goodthing里的village表)
/usr/local/mysql/bin/mysqldump -u${username} -p${password} --quick --events --databases ${dbName} --ignore-table=goodthing.village --ignore-table=goodthing.area --flush-logs --delete-master-logs --single-transaction > $dumpFile
# 打包
/bin/tar -zvcf $gzDumpFile $dumpFile
/bin/rm $dumpFile

endTime=`date +"%Y年%m月%d日 %H:%M:%S"`
echo 开始:$beginTime 结束:$endTime $gzDumpFile succ >> $logFile

# 删除所有增量备份
cd $bakDir/daily
/bin/rm -f *

增量备份

# 检查log_bin是否开启
mysql> show variables like '%log_bin%';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| log_bin                         | OFF   |
| log_bin_basename                |       |
| log_bin_index                   |       |
| log_bin_trust_function_creators | OFF   |
| log_bin_use_v1_row_events       | OFF   |
| sql_log_bin                     | ON    |
+---------------------------------+-------+
6 rows in set (0.01 sec)

#开启 log_bin,并重启mysql
##编辑 mysql 的配置文件 vim /etc/my.cnf,在 mysqld 下面添加下面2条配置
[mysqld]
log-bin=/var/lib/mysql/mysql-bin
server_id=152

##重启mysql
service mysqld restart

#备份
##进入mysql命令行,执行 show master status;
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      430 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

##比如当前数据库test的bk_user只有2条记录
mysql> select * from test.bk_user;
+----+------+------+------+
| id | name | sex  | age  |
+----+------+------+------+
|  1 | 小明 | 男   |   25 |
|  2 | 小红 | 女   |   21 |
+----+------+------+------+
2 rows in set (0.00 sec)

##插入一条新的记录
mysql> insert into test.bk_user(name, sex, age) values('小强', '男', 24);
Query OK, 1 row affected (0.02 sec)
mysql> select * from test.bk_user;
+----+------+-----+-----+
| id | name | sex | age |
+----+------+-----+-----+
|  1 | 小明 | 男  |  25 |
|  2 | 小红 | 女  |  21 |
|  5 | 小强 | 男  |  24 |
+----+------+-----+-----+
3 rows in set (0.03 sec)

##执行命令mysqladmin -uroot -p密码 flush-logs,生成并使用新的日志文件
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000004 |      154 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

--------------------------------------------------------------------

恢复增量备份

#首先假装误删数据库记录
mysql> delete from test.bk_user where id=4;
Query OK, 1 row affected (0.01 sec)

mysql> select * from test.bk_user;
+----+------+------+------+
| id | name | sex  | age  |
+----+------+------+------+
|  1 | 小明 | 男   |   25 |
|  2 | 小红 | 女   |   21 |
+----+------+------+------+
2 rows in set (0.00 sec)

#从备份的日志文件mysql-bin.000003中恢复数据
[root@centos56 ~]# mysqlbinlog --no-defaults /var/lib/mysql/mysql-bin.000003 | mysql -uroot -p test
Enter password: 
ERROR 1032 (HY000) at line 36: Can't find record in 'bk_user'

##如果你也遇到这个问题的话,不妨修改 /etc/my.cnf 配置试试。
##我在server_id那一行下添加了 slave_skip_errors=1032 ,然后就执行成功了,不再报错

mysql> select * from test.bk_user;
+----+------+------+------+
| id | name | sex  | age  |
+----+------+------+------+
|  1 | 小明 | 男   |   25 |
|  2 | 小红 | 女   |   21 |
|  5 | 小强 | 男   |   24 |
+----+------+------+------+
3 rows in set (0.00 sec)

增量定时备份脚本

#!/bin/bash

# 增量备份时复制mysql-bin.00000*的目标目录,提前手动创建这个目录
BakDir=/home/mysql/backup/daily
# 日志文件
LogFile=/home/mysql/backup/bak.log

# mysql的数据目录
BinDir=/var/lib/mysql-bin
# mysql的index文件路径,放在数据目录下的
BinFile=/var/lib/mysql-bin/mysql-bin.index

# 这个是用于产生新的mysql-bin.00000*文件
/usr/local/mysql/bin/mysqladmin -uroot -ptencns152 flush-logs

Counter=`wc -l $BinFile | awk '{print $1}'`
NextNum=0
# 这个for循环用于比对$Counter,$NextNum这两个值来确定文件是不是存在或最新的
for file in `cat $BinFile`
do
        base=`basename $file`
        NextNum=`expr $NextNum + 1`
        if [ $NextNum -eq $Counter ]
        then
                echo $base skip! >> $LogFile
        else
                dest=$BakDir/$base
                #test -e用于检测目标文件是否存在,存在就写exist!到$LogFile去
                if(test -e $dest)
                then
                        echo $base exist! >> $LogFile
                else
                        cp $BinDir/$base $BakDir
                        echo $base copying >> $LogFile
                fi
        fi
done

echo `date +"%Y年%m月%d日 %H:%M:%S"` $Next Bakup succ! >> $LogFile

定时备份

# 每个星期日凌晨3:00执行完全备份脚本
0 3 * * 0 /bin/bash -x /root/bash/Mysql-FullyBak.sh >/dev/null 2>&1

# 周一到周六凌晨3:00做增量备份
0 3 * * 1-6 /bin/bash -x /root/bash/Mysql-DailyBak.sh >/dev/null 2>&1

2.最佳实践

#表分析
--当我们需要统计表的记录行数,比如执行:show table status;
--或者需要避免mysql选错索引的时候,我们可以执行表分析操作:analyze table a_test;

#重建表
--当一个表因为增删改操作,导致页空洞,而造成空间浪费的时候,我们可以执行重建表
--备注:mysql的delete操作只是标记删除,即被删除数据的位置可复用,而不是说真正的删除了数据。因此有时候我们会发现delete from t后,表空间大小并没有改变
--需要注意:重建表DDL,不是ONLINE的
--重建表方式一:alter table a_test engine=innodb;
--重建表方式二:optimize table a_test;

# mysql中redo log与binlog
--redo log 是重做日志,对于mysql数据库来说,使用了WAL技术,一条语句更新成功的标志是:更新内存,写入redo log即可
--redo log重做日志是两阶段提交,是Innodb引擎独有,有crash safe能力,即保证数据不丢失。两阶段提交保证数据一致性
--binlog是server层实现,所有存储引擎都可以使用,主要用于归档,主从复制等能力
--redo log的两阶段提交,写入redo log后预提交(prepare),写入binlog后再次提交(commit)
--mysql崩溃恢复后,通过两阶段提交保证不丢失数据,因为在redo log与binlog之间通过xid关联在一起
--在生产环境中,一般将redo log设置为四个文件,每个文件大小 1个G。redo log是循环写,binlog是追加写

#误删除表以后,该如何恢复?
--找出最近一次全量备份,从该备份恢复一个临时库
--从备份时间点开始,将线上库的binlog日志取出来,重放到临时库误删表之前的时刻
--这样即得到一个与线上库误删表前一致的临时库,再从临时库取出误删表,写入线上库即可

	
#关于索引的选择
--在实际项目中,如果业务应用保证了唯一性,那么我们推荐使用普通索引,因为普通索引可以利用change buffer,效率会更高(减少了访问操作磁盘)
--比如说归档库,我们就可以使用普通索引,而不使用唯一索引,因为归档库中的数据已经保证了唯一性
--在mysql中优化器选择索引的参考因素:基数(采样统计得到)、是否排序、是否使用临时表

#怎么给字符串添加索引
--添加前缀索引,或者reverse操作,或者冗余一个字段存储hash值
--关于前缀字符长度定义,比如允许损失区分度比例:5%,那么需要找出统计字符串子串长度值,比如L4-L7的数量,找出不小于L*95%的值。这里的L指字符串子串的长度
SELECT
	count(DISTINCT LEFT(NAME, 2)) AS L2,
	count(DISTINCT LEFT(NAME, 3)) AS L3,
	count(DISTINCT LEFT(NAME, 4)) AS L4,
	count(DISTINCT LEFT(NAME, 5)) AS L5,
	count(DISTINCT LEFT(NAME, 6)) AS L6
FROM
	a_test;

#为什么有时候mysql会“抖”一下,即会变卡顿
--原因一:可能是redo log写满了,需要刷脏页,导致所有的更新都被阻塞
--原因二:内存不够用了,需要刷脏页

#关于count(*)、count(1)、count(主键字段)、count(普通字段)的性能对比
--大的原则:server层需要什么,存储引擎层就返回什么
--count(*),存储引擎层取行,并返回,server层直接加1
--count(1),存储引擎层取行并返回1,server直接加1
--count(主键字段),存储引擎层取行,解析主键字段,拷贝主键字段,server层加1
--count(普通字段),存储引擎层取行,解析普通字段,拷贝普通字段,server层判断字段不为空,然后加1

--因此性能从低到高:count(普通字段)-->count(主键字段)-->count(1)=count(*)


# 关闭整个库查询缓存,通过sql_cache明确需要缓存的sql
set query_cache_type=DEMAND
select sql_cache * from my_user where id=1

#redo log持久化到磁盘参数配置
set innodb_flush_log_at_trx_commit=1
#binlog 持久化到磁盘参数配置
set sync_binlog=1
#事务隔离级别参数配置,oracle默认读提交,mysql默认可重复读
set tx_isolation=REPEATABLE-READ

#事务启动方式
1.显示启动: begin/start transaction....commit/rollback
2.set autocommit=0,关闭自动提交,必须通过commit/rollback提交或者回滚
3.在实践中,推荐打开自动提交,即set autocommit=1

#查询长事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60


#排序相关-----------------------------
/* 打开optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on'; 

/* @a保存Innodb_rows_read的初始值 */
select VARIABLE_VALUE into @a from  information_schema.session_status where variable_name = 'Innodb_rows_read';

/* 执行语句 */
select city, name,age from t where city='杭州' order by name limit 1000; 

/* 查看 OPTIMIZER_TRACE 输出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G

/* @b保存Innodb_rows_read的当前值 */
select VARIABLE_VALUE into @b from information_schema.session_status where variable_name = 'Innodb_rows_read';

/* 计算Innodb_rows_read差值 */
select @b-@a;


# 构造for update问题,关于间歇锁,只在RR级别下有,在RC级别下无
# 如果将事务隔离级别设置成RC,需要考虑数据不一致问题(幻读),建议将binlog设置为row格式,即set binlog_format=row
--创建表,及初始化数据
CREATE TABLE `t1` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t1 values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

select * from t1;

--手动开启事务,且where字段无索引,现象:锁住全表
begin/start transaction;
select * from t1 where d=5 for update;
commit;

insert into t1 values(30,30,30);

--手动开启事务,且where字段有索引,现象:不锁住全表,锁住索引范围,解决幻读:Gap Lock
begin/start transaction;
select * from t1 where c=5 for update;
commit;

insert into t1 values(6,6,6);--->锁住
insert into t1 values(26,26,26);--->不锁

#插入数据,如果存在,即更新
insert … on duplicate key update 

#sql语句改写

mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1");

call query_rewrite.flush_rewrite_rules();