你好,我是俊达。
这一讲主要是对专栏基础篇中思考题的一个统一的答疑。实际上在评论区中, 我也看到了同学们对很多思考题的精彩解答。这里做一个汇总和补充,方便大家查看。
第1讲
问题:早期的版本中,登录服务器本地的MySQL默认不需要密码。这存在一定的安全风险,因为你只要能登录到数据库服务器,就能访问这台服务器上的MySQL数据库。从5.7版本开始,给数据库的root账号设置了默认密码,首次登录时需要先修改密码。在部署一套MySQL数据库环境时,小明按规范修改了数据库root账号的密码,但是当时忘了将root密码记录下来,因此小明向你寻求帮助。你有办法帮小明解决这个问题吗?
@飒飒秋风 在评论区提供了这个问题的标准处理方法。
- 关闭MySQL。
1 | kill <mysqld进程的pid> |
- 添加–skip-grant-tables参数,启动MySQL。
1 | mysqld_safe --defaults-file=my.cnf --skip-grant-tables --skip-networking & |
- 执行flush privileges,加载权限表。
1 | flush privileges; |
- 执行alter user命令,修改root用户密码
1 | alter user 'root'@'localhost' identified by 'password'; |
- 重新启动MySQL,去掉–skip-grant-tables参数。
使用参数–skip-grant-tables启动的MySQL,不校验用户名和密码,也不校验用户权限,可以执行任何操作。因此一般建议同时加上–skip-networking参数,这样就只能在服务器本地登陆数据库。不过8.0使用–skip-grant-tables时,默认就只能本地访问了。
要先执行flush privileges命令加载权限表,然后才能修改密码。修改完密码后,重新启动MySQL,这次去掉–skip-grant-tables参数。
第2讲
问题:一般情况下,我们都建议将数据库部署到内网,因为将数据库暴露到公网上有比较大的安全风险。但是你的公司有一个特殊的业务,就是需要通过公网访问MySQL数据库。请你评估下将数据库放到公网有哪些风险?你应使用哪些方法来尽量保证数据库和数据的安全?
@Geek_0126 @Amosヾ的留言中,都讲到了数据库公网访问的风险和解决方法。
风险1. 数据库被各种手段攻击,用户密码被破解。
为了避免风险,首先在网络层限制来源IP。这和创建MySQL用户时指定访问IP还不一样。创建用户时指定IP,并不会阻止在网络层面访问数据库端口。数据库的用户密码要有一定的复杂度,避免使用简单密码。同时也要遵循最小权限原则。
风险2. 传输的数据被被拦截捕获。
如果MySQL客户端和服务端之间传输的TCP数据被捕获,并且没有使用加密传输,那么很容易解析出客户端发送的命令、服务端返回的数据。解决这个问题最好的办法是使用加密传输。
还可以在创建用户时,指定用户必须使用加密传输,还可以要求验证客户端证书。
1 | create user 'user1'@'%' identified by 'somepass' require ssl; |
第3讲
问题:开发同学反馈访问数据库总是报错,并提供了一些报错日志。你应该怎么来分析和解决这个问题呢?
1 | ERROR druid.sql.Statement -{conn-10094, stmt-26348} execute error. SELECT 1 FROM DUAL |
这是使用JDBC访问MySQL经常会遇到的一个报错。报错信息“N milliseconds ago”里的时间是一个关键的信息。比如上面例子中的“899,883 milliseconds ago”,说明15分钟连接就断开了。需要检查下wait_timeout的值,是不是真的是15分钟。
wait_timeout是一个会话级的变量,每个会话可以设置成不同的值。因此需要确认被断开的连接,wait_timeout是怎么设置的。8.0中可以通过performance_schema.variables_by_thread表,查看其他会话的变量值。
1 | mysql> select * from performance_schema.variables_by_thread |
你还可以使用其他一些方法来验证空闲超时时间。比如查看processlist,看看command为Sleep的连接,最大的时间是多少。或者执行select sleep(N),看看查询是否能正常完成。
在上面这个例子中,会话的wait_timeout的设置是几万秒,并不是15分钟。但是超过15分钟,连接就会被断开,Processlist中看不到空闲时间超过15分钟的会话。执行sleep(901)也无法成功。
最终定位的原因是使用了Nginx来访问MySQL,而代理的超时时间配置成了900秒。连接池的keepalive配置也没有生效。
第4讲
问题:我们写SQL语句时,关键字一般不区分大小,不同的人可能有不同的习惯。对于库名、表名、列名,不同的数据库有不同的处理方法。比如Oracle中,表名默认不区分大小写。在MySQL中,根据操作系统的不同,表名就可能会区分大小写。
1 | mysql> show tables; |
参数lower_case_table_names可用来控制表名是否区分大小写。
1 | mysql> show variables like '%lower%'; |
MySQL 8.0中,这个参数只能在数据库初始化之前设置,之后就不能再修改了,修改后数据库都无法启动。
1 | [ERROR] [MY-011087] [Server] Different lower_case_table_names settings for server ('1') and data dictionary ('0'). |
你觉得这个参数应该怎样设置?原因是什么?
MySQL会根据操作系统和底层文件系统的特性,来选一个默认的lower_case_table_names参数。但是我觉得这并不是很好的一种做法。
一般我们写SQL时,并不会特别注意表名的大小写,不同的人可能有不同的习惯,有时使用大写,有时使用小写。比如下面这3个SQL,我相信大多数人都会认为这其实是同一个SQL。
1 | select * from tab; |
但是linux下,如果使用默认的lower_case_table_names=0,这3个SQL访问了完全不同的3个表。
1 | mysql> show tables; |
这除了引起混乱,还有其他什么重要的作用吗?
有时,这也会给跨操作系统的数据库迁移带来麻烦,比如将MySQL从Windows迁移到Linux,或者反过来迁移。我的建议是所有环境的MySQL都将参数lower_case_table_names设置为1,统一转换成小写。
第5讲
问题:你刚刚接到了1个需求,要开发一个员工管理系统。该系统计划使用MySQL数据库,设计人员提供了表结构,其中就包括下面这2个表。作为一位资深的MySQL使用者,你觉得这2个表存在哪些问题?你会怎么改进呢?
1 | create table t_employee( |
这个问题 评论区中@吃苹果的考拉 @123 @Geek_0126 的回答都比较完整了。
t_employee表:
- emp_name varchar长度不用设置得这么长
- birth_day 使用日期类型更好
- age 使用smallint也足够了
- gender 可以使用tinyint或enum
- photo 建议数据库中值保存一个文件地址。图片存储在分布式文件系统中。
t_emp_salary表:
- emp_id和t_employee保持一致
- effect_date使用日期类型
- salary使用Decimal或int类型
- 联合主键的问题。
第6讲
问题:MySQL的备库复制中断了,查看错误信息,发现是有一个建表的语句报错了。
1 | Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'c1a67221-f9fc-11ed-bffd-fa8338b09400:106' at master log binlog.000020, end_log_pos 4203259. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any. |
1 | mysql> select * from performance_schema.replication_applier_status_by_worker\G |
但是到主库上查看后,发现这个表创建成功了。
1 | mysql >show create table t_inno1\G |
为什么会出现这种情况呢?
这里关键的报错信息是“Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help.”。
建表是否成功和参数innodb_strict_mode的设置有关。分析这个问题时,先对比主库和备库上innodb_strict_mode的设置。
innodb_strict_mode可以在会话级别修改,主库上将innodb_strict_mode的会话值改成OFF,然后建表成功了。但是MySQL并没有把这个会话变量记录在Binlog中,备库执行时,innodb_strict_mode=ON,因此失败了。
一种解决方法是备库上临时将innodb_strict_mode的全局值修改成OFF,再开启复制。
第7讲
问题:某一个项目发布时,执行了以下数据初始化脚本。
1 | mysql> CREATE TABLE `t_b` ( |
对数据进行检查后,没有发现任何问题。
1 | mysql> select * from t_b; |
但是另外一个同事查询这个表时,发现无论怎么设置字符集,查出来都是乱码。
1 | mysql> set names utf8; |
这位同事找到了你,请你帮忙分析为什么会出现这样的问题?应该如何解决呢?
@Shelly 在评论区中给出了这个问题的根本原因,set names指定的字符集和终端实际的编码不一致。
utf-8的终端下,执行set names gbk后,虽然写入、读取中文字符好像没有任何问题,但是实际上表里面存的数据已经不对了。
1 | mysql> select a, hex(a), char_length(a) from t_b; |
“中文符号”的GBK编码是D6D0 CEC4 B7FB BAC5,但是表里面存的其实是“中文符号”的UTF8编码。这和表的字符集不一致了,因此执行char_length获取的字符数也不对了。
这种问题要从源头上解决,把错误的数据修正过来。比如删除有问题的数据,设置正确的字符集后,重新写入数据。
第8讲
问题:源库中有一个大表,表结构定义如下:
1 | create table big_table( |
这个表总共有3000万行数据,平均行长度大约为2K。现在需要将这个表复制到目标库。源库和目标库都是MySQL。有一台4核8G的中转机器供你使用。请问你会怎么解决这个需求?如果要你写一段程序来完成这个任务,需要注意什么?如何提高数据复制的速度?
评论区中@123 @dream 给出了处理这个问题的一些思路。这里我总结一下。
- 使用多个线程来处理。一个线程负责查询数据,将记录缓存在内存的一个队列中。再使用一个或多个线程从队列中获取数据,执行Insert操作。Insert时,可以使用MySQL的批量插入,将多条数据(比如100条)拼接成一个Insert SQL再执行。
- 由于表的数据量比较大,客户端无法一次性将所有结果集都缓存起来,要么使用分页的SQL,要么使用流式处理。这个表使用了组合主键,分页SQL还不太好写。如果是用了自增主键,分页SQL就很容易写了,而且也可以很方便地使用多个线程分段查询数据。
第9讲
问题:这一讲我们提到了mysqldump的一个限制:导出的数据都存在同一个文件中,不方便并行导入。一次紧急故障中,需要将mysqldump备份出来的数据恢复出来,数据库比较大,单线程恢复的话,耗时又会比较久,你有哪些办法来加快恢复的速度?
@范特西 提到了可以设置数据库的一些参数来提升恢复的速度。
其实mysqldump生成的文件,有一些特定的格式,比如看下面这个例子,创建数据库、创建表、Insert数据前,都有相应的注释文本。你可以根据这个特点,将dump文件分割成多个文件。
1 | -- |
第10讲
问题:由于公司的策略,需要将一个核心业务系统的Oracle数据库迁移到MySQL。这个Oracle数据库大概有1T数据,迁移过程中,要尽可能缩短业务停机的时间,业务方能接受的最大停机时间在1~2小时之内。请你设计一个方案,将数据平滑地迁移到MySQL。你需要考虑全量数据如何迁移,业务运行期间新产生的数据如何迁移。
@123 @ls @Geek_0126 在评论区提供了一些方法,比如使用一些数据同步的工具。
这个问题和第8讲的思考题有一些类似的地方,都需要将数据从一个数据库迁移到另外一个数据库。这个问题多了一些限制,源库变成了Oracle,源端和目标端使用了不同类型的数据库,而且还要考虑增量数据,要尽量减少停机时间。
这类异构数据迁移要考虑的几个问题。
数据类型:不同的数据库在数据类型上会有一些区别,需要做一个映射。比如Oracle的Number类型,根据使用场景,可以对应到MySQL中的int系列类型或decimal。
全量数据处理:全量数据的同步本质上就是Select+Insert。不过由于数据量大,要考虑效率问题。使用sqluldr2将Oracle的数据导出成文本,然后再使用load data命令导入到MySQL是效率比较高的一种方法。
增量数据处理:增量数据有几种不同的处理方式。
- 一种方式是在业务侧处理,比如使用双写,或者将源端所有的数据修改都记录一份到消息队列中,再写一个程序订阅消息,将变更同步到目标库。这对应用有比较高的要求。
- 另一种方式是解析Oracle的Redo日志,同步增量数据。这通常需要借助一些工具,比如OGG。自己解析Redo有比较高的技术门槛,不同版本Oracle Redo格式可能还会有一些差异。可以尝试Oracle的LogMiner。
- 还有一种方式是使用物化试图,或者给源端的表建立触发器,将数据变动记录到日志表,根据日志表的数据来增量同步数据。这会对源端的写入带来额外的负担。
综合来看,使用一些成熟的工具,是实施异构增量迁移比较好的一个选择。
第11讲
问题:只修改元数据的DDL、INSTANT DDL执行速度通常都很快,但是这些DDL执行也是需要获取元数据锁的,比如下面这个例子:
- 会话1开启一个事务,执行一个select for update操作。
1 | mysql> desc t_ddl; |
- 会话2 执行一个instant DDL。
mysql> alter table t_ddl add c int, algorithm=instant;
- 会话3 执行一个普通的select操作。
mysql> select * from t_ddl limit 1;
你会发现,会话2和会话3都被阻塞了。从processlist可以看到,它们都在等待元数据锁。(输出结果做了简化)
1 | mysql> show processlist; |
从performance_schema.metadata_locks也可以看到t_ddl表的元数据持有和请求情况。
1 | mysql> select * from metadata_locks where object_name = 't_ddl'; |
还有哪些情况会导致DDL无法获取到元数据锁?怎么快速定位到元数据锁的阻塞源?
@叶明 @Shelly @Geek_0126 提供了这个问题的解答。这里再简单总结下。
下面这几种情况都会阻塞DDL。
- 有慢SQL还在运行中,访问了这个表。
- 事务中,访问过这个表(查询或修改),并且事务还没有提交。
- 全局锁,比如flush tables with read lock,lock instance for backup
- 表锁,lock tables
从sys.schema_table_lock_waits、performance_schema.metadata_locks表查看表锁、元数据锁。
第12讲
问题:gh-ost是比较知名的一款在线DDL工具,在实现上也非常有特色。gt-ost在执行DDL变更时,不需要给源表建触发器,而是通过BINLOG来捕捉DDL变更期间发生过变化的数据。我尝试在测试环境做了一个实验。
1 | gh-ost -alter "alter table employees_bak modify hire_date date" \ |
在执行gh-ost前,我先开启了general_log,最终在general log中发现有以下这几类SQL。
- create table like
1 | create /* gh-ost */ table `employees`.`_employees_bak_gho` |
- alter table
alter /* gh-ost */ table employees._employees_bak_gho modify hire_date date
- select
1 | select /* gh-ost `employees`.`employees_bak` iteration:0 */ |
- insert ignore into
1 | insert /* gh-ost `employees`.`employees_bak` */ |
- replace into
1 | replace /* gh-ost `employees`.`_employees_bak_gho` */ into |
- rename table
rename /* gh-ost */ table employees.employees_bak to employees._employees_bak_del, employees._employees_bak_gho to employees.employees_bak
上面的这几类SQL,分别起到了什么作用?insert ignore into和replace into的执行顺序,对最终数据的一致性有影响吗?执行insert ignore into … select from … 的时候,为什么要加上lock in share mode?
@叶明 @Shelly @123 在评论区提供了这个问题的解答。
- create table like:创建临时表,复制表结构。
- alter table:根据命令更改表表结构。
- select:并发读取数据(看到了偏移量,应该是多线程)写入临时表。
- insert ignore into:从源表select出来的数据,使用insert ignore into插入到临时表中,忽略重复的数据。
- update:binlog中的Update事件解析为Update语句。
- replace into:binlog中的Insert事件解析为replace into语句,将增量数据写到临时表。
- delete:binlog中的Delete事件解析为Delete语句。
- rename table:将临时表变更为生产表。
lock in share mode是为了保证影子表数据和原表一致。 Binlog可见和事物可见之间存在一个时间差,如果不加lock in shared mode,源库执行delete语句时,binlog中读到了delete语句,但select可能会读取到DELETE之前的数据。这样,如果先对影子表执行了delete,然后再执行insert ignore into时,会把本来应该已经delete掉的数据,插入到影子表,就多数据了。
第13讲
问题:国庆节假期,DBA小明突然接到大量数据库告警,登录数据库执行SHOW PROCESSLIST后,发现大量会话被阻塞了。下面提供了部分会话的信息。请你根据这些信息,帮小明一起分析下,为什么会出现这样的问题?应该怎么解决这个问题呢?有哪些地方可以改进?
1 | Id: 1842782 |
@叶明 @binzhang 提供了这个问题的解答。我再补充一些信息。
- Id为1657130的线程是这里的阻塞源。这个SQL执行了2天多还没完成。当时用的应该是MySQL 5.1还是5.5,IN子查询的效率比较低,以主查询作为驱动表,但是主查询上又没有其他有效的过滤条件。
- Id为1044的线程执行了Flushing tables FLUSH TABLES,由于Id为1657130的线程一直在执行SQL,因此被阻塞了。
- Id为1044的User是system user,这比较有意思,因为这是一个SQL线程,说明FLUSH TABLES这个操作是从主库中复制过来的。这实际上是一个MM架构的环境,备库上使用mysqldump备份数据,发起了FLUSH TABLES操作,复制到了业务的主库。
- Id为1842782的线程访问表时,被阻塞了。
这里其实还暴露了当时的监控不够完善。ID为1657130的线程,执行超过了2天。SQL执行时间超过一定的阈值就可以告警出来。
第14讲
问题:有一个生产环境的系统,有时候会出现数据库连接超时的报错。从报错的信息看,超时应该和网络有点关系。遇到这样的问题,你会使用哪些方法来定位原因呢?
如果你知道数据库的连接地址,用户名和密码,就可以使用客户端测试数据库是否能正常连接。有时候,我们可能并不真正知道数据库的连接信息,比如连接地址可能硬编码在代码中了,或者连接地址在多个配置文件中个呢配置,或者连接地址信息存在在某个数据库中。
有时候,可能是因为连接到了错误的IP地址。有什么办法查看应用连接了哪个IP呢?有几种方法可以尝试。
- 使用strace,或者其他trace工具(如systemtap),查看应用程序的系统调用,分析系统调用的参数。
- 使用net stat或ss命令,查看系统上的TCP连接。如果程序连接了错误的IP,可能会看到有TCP连接的状态是SYN_SENT。
- 使用tcpdump抓取和分析网络流量。
第15讲
问题:MySQL中存在这么一个现象,平时执行得好好的SQL,在数据库很繁忙的时候,执行效率也会变得很差,当然,这可以理解。你从Processlist或慢SQL日志中看到执行耗时比较长的SQL,其中有些是引起数据库性能问题的罪魁祸首,有些则是受害者。你应该怎么区分这两种情况,找到真正需要优化的那些SQL呢?
@叶明 @范特西 提供很好的一个思路,根据SQL的扫描函数来判断。还可以根据SQL的执行历史来进行分析。
如果有一个空闲的备库,可以到备库上执行SQL,对比执行时间。
第16讲
问题:有些情况下,我们可能会使用逻辑的方式来升级数据库,比如将线下低版本的数据库迁移到云上高版本的实例中。使用逻辑的方式升级或降级数据库,可能会存在哪些问题?有哪些需要注意的地方?
使用逻辑备份,比如mysqldump导出的sql文件,到低版本的MySQL上执行时,可能会由于数据类型、SQL语法等不兼容问题,出现导入报错的情况。如果高版本的mysql中使用了一些新的特性,在低版本上不支持,就可能会遇到问题。