mysql面试问题
-
第一范式(确保每列保持原子性) 无重复的列
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
上表所示的用户信息遵循了第一范式的要求,这样在对用户使用城市进行分类的时候就非常方便,也提高了数据库的性能。
-
第二范式(确保表中的每列都和主键相关)
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键,如下表所示。
订单信息表
这样就产生一个问题:这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品价格等信息不与该表的主键相关,而仅仅是与商品编号相关。所以在这里违反了第二范式的设计原则。
而如果把这个订单信息表进行拆分,把商品信息分离到另一个表中,把订单项目表也分离到另一个表中,就非常完美了。如下所示。
这样设计,在很大程度上减小了数据库的冗余。如果要获取订单的商品信息,使用商品编号到商品信息表中查询即可。
-
第三范式(确保每列都和主键列直接相关,而不是间接相关)
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。如下面这两个表所示的设计就是一个满足第三范式的数据库表。
这样在查询订单信息的时候,就可以使用客户编号来引用客户信息表中的记录,也不必在订单信息表中多次输入客户信息的内容,减小了数据冗余。
②事务(Transaction)及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性:
1.原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。 2.一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。 3.隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。 4.持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
并发事务处理带来的问题
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。 1.脏读(Dirty Reads):一个事务可以读取另一个尚未提交事务的修改数据。 2.不可重复读(Non-Repeatable Reads):同一个事务中,多次查询某个数据,却得到不同的结果。 3.幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
③varchar与char和varchar2区别
-
varchar与char
-
定长和变长
char 表示定长,长度固定,varchar表示变长,即长度可变。char如果插入的长度小于定义长度时,则用空格填充;varchar小于定义长度时,还是按实际长度存储,插入多长就存多长。
因为其长度固定,char的存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好相反,以时间换空间。
-
存储的容量不同
对 char 来说,最多能存放的字符个数 255,和编码无关。 而 varchar 呢,最多能存放 65532 个字符。varchar的最大有效长度由最大行大小和使用的字符集确定。整体最大长度是 65,532字节。
-
-
varchar和varchar2
-
varchar是标准sql里面的。 varchar2是oracle提供的独有的数据类型。
-
varchar对于汉字占两个字节,对于英文是一个字节,占的内存小,varchar2都是占两个字节。
-
varchar对空串不处理,varchar2将空串当做null来处理。
-
varchar存放固定长度的字符串,最大长度是2000,varchar2是存放可变长度的字符串,最大长度是4000。
-
如果是要跟换不同的数据库,例如mysql,那么就用varchar,如果就用oracle,那么用varchar2比较好一点。
-
④float、double、decimal的区别
-
float类型表示单精度浮点数值,double类型表示双精度浮点数值,float和double都是浮点型,而decimal是定点型;
-
MySQL 浮点型和定点型可以用类型名称后加(M,D)来表示,M表示该值的总共长度,D表示小数点后面的长度,M和D又称为精度和标度,如float(5,2)的 可显示为999.99,MySQL保存值时会进行四舍五入,如果插入999.009,则结果为999.01;
-
float和double在不指定精度时,默认会按照实际的精度来显示,而DECIMAL在不指定精度时,默认整数为10,小数为0。
先创建一个表:
CREATE TABLE test(
f FLOAT(10,2) DEFAULT NULL,
d DOUBLE(10,2) DEFAULT NULL,
de DECIMAL(10,2) DEFAULT NULL
);
insert三条数据如下所示:
INSERT INTO test(f,d,de) VALUES(1.23,1.23,1.23);
INSERT INTO test(f,d,de) VALUES(1.234,1.234,1.23);
INSERT INTO test(f,d,de) VALUES(1.234,1.234,1.234);
然后修改表结构并新增一条数据:
ALTER TABLE test MODIFY f FLOAT;
ALTER TABLE test MODIFY d DOUBLE;
INSERT INTO test(f,d,de) VALUES(1.234,1.234,1.234);
此时计算求和值如下图所示:
SELECT SUM(f),SUM(d),SUM(de) FROM test;
float和double求SUM的结果都是不精确的,只有decimal求SUM得到的是精准数值:
所以,decimal 类型是适合财务和货币计算的128位数据类型。
⑤左连接、右连接与内连接的区别
MYSQL中可以通过内外键连接,将有关系的表中数据合并到一起进行条件筛选:
首先创建两个新表,数据如下:
student 表数据:
score 表数据:
可以看到students表中stu_id为16048008的记录对应score表没有数据;
-
当进行内连接时,系统会自动忽略两个表中对应不起来的数据:
-- 显示内连接所有数据:
SELECT * FROM students st INNER JOIN score sc ON st.sid=sc.stu_id;数据太多,只截图最末尾的:
可以看到数据只显示到16048007,16048008的并没有显示,所以内连接只显示所有有关联的数据。
-
左连接,显示关键词left左边表中的所有数据,右边表数据数据少了补NULL值,数据多了不显示;
-- 左外链接 left
SELECT * FROM students st LEFT JOIN score sc ON st.sid=sc.stu_id;可以看到,16048008在右边score表中没有数据,系统用NUll补齐。
SELECT * FROM score sc LEFT JOIN students st ON st.sid=sc.stu_id;
上图是我们将students表和score表换了一下位置的运行结果,可以看出,本来右边表16048008记录,因为在左边表钟没有数据对应,所以被删除了,可以看出,左连接是以左边表中数据为参照,显示左边表中所有数据,右边表只显示与左边表对应的数据。少了补NULl,多了删除;
同理,右连接是以右边为参照,左边少了补NULL,多了删除,这里就不多说了;
-
结论
-
内连接,显示两个表中有联系的所有数据;
-
左连接,以左表为参照,显示所有数据;
-
右连接,以右表为参照显示数据;
-
⑥mysql索引实现
目前大部分数据库系统及文件系统都采用B-Tree(B树)或其变种B+Tree(B+树)作为索引结构。B+Tree是数据库系统实现索引的首选数据结构。在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的。
-
索引
索引是一种高效获取数据的存储结构,例:hash、 二叉、 红黑。
Mysql采用B+Tree原因:
若仅仅是 select * from table where id=45,上面三种算法可以轻易实现,但若是select * from table where id<6 , 就不好使了,它们的查找方式就类似于"全表扫描",因为他们的高度是不可控的(如下图)。B+Tree的高度是可控的,mysql通常是3到5层。
注意:B+Tree只在最末端叶子节点存数据,叶子节点是以链表的形势互相指向的。
-
MyISAM 索引实现
-
主键索引
MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址。下图是 MyISAM 索引的原理图:
这里设表一共有三列,假设我们以Col1为主键,上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。
-
辅助索引(Secondary key)
在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:
同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。
-
-
InnoDB索引实现
InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。
-
主键索引:
MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
上图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
-
InnoDB的辅助索引
InnoDB的所有辅助索引都引用主键作为data域。例如,下图为定义在Col3上的一个辅助索引:
InnoDB 表是基于聚簇索引建立的。因此InnoDB 的索引能提供一种非常快速的主键查找性能。不过,它的辅助索引(Secondary Index, 也就是非主键索引)也会包含主键列,所以,如果主键定义的比较大,其他索引也将很大。如果想在表上定义 、很多索引,则争取尽量把主键定义得小一些。InnoDB 不会压缩索引。
文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
-
-
总结
不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
InnoDB索引和MyISAM索引的区别:
一是主索引的区别,InnoDB的数据文件本身就是索引文件。而MyISAM的索引和数据是分开的。
二是辅助索引的区别:InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别。
⑦建立的索引判断是否生效
使用方法,在select语句前加上explain就可以了:
EXPLAIN SELECT surname,first_name form a,b WHERE a.id=b.id
EXPLAIN列的解释:
table:显示这一行的数据是关于哪张表的。
type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、index和ALL。
possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句。
key: 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MySQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MySQL忽略索引。
key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好。
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:MySQL认为必须检查的用来返回请求数据的行数。
Extra:关于MySQL如何解析查询的额外信息。
-
MYSQL索引无效和索引有效的详细介绍
-
WHERE字句的查询条件里有不等于号(WHERE column!=...),MYSQL将无法使用索引
-
类似地,如果WHERE字句的查询条件里使用了函数(如:WHERE DAY(column)=...),MYSQL将无法使用索引
-
在JOIN操作中(需要从多个数据表提取数据时),MYSQL只有在主键和外键的数据类型相同时才能使用索引,否则即使建立了索引也不会使用
-
如果WHERE子句的查询条件里使用了比较操作符LIKE和REGEXP,MYSQL只有在搜索模板的第一个字符不是通配符的情况下才能 使用索引。比如说,如果查询条件是LIKE 'abc%',MYSQL将使用索引;如果条件是LIKE '%abc',MYSQL将不使用索引。
-
在ORDER BY操作中,MYSQL只有在排序条件不是一个查询条件表达式的情况下才使用索引。尽管如此,在涉及多个数据表的查 询里,即使有索引可用,那些索引在加快ORDER BY操作方面也没什么作用。
-
如果某个数据列里包含着许多重复的值,就算为它建立了索引也不会有很好的效果。比如说,如果某个数据列里包含了净是些诸如“0/1”或“Y/N”等值,就没有必要为它创建一个索引。
-
索引有用的情况下就太多了。基本只要建立了索引,除了上面提到的索引不会使用的情况下之外,其他情况只要是使用在WHERE条件里,ORDER BY 字段,联表字段,一般都是有效的。
建立索引要的就是有效果。 不然还用它干吗? 如果不能确定在某个字段上建立的索引是否有效果,只要实际进行测试下比较下执行时间就知道。
-
⑧Mysql事务隔离级别
-
事务的并发问题
-
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
-
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
-
幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
-
-
事务的隔离级别
事务隔离级别 脏读 不可重复读 幻读 读未提交(read-uncommitted)【RU】 是 是 是 读已提交(read committed)【RC】 否 是 是 可重复读(repeatable-read)【RR】 否 否 是 串行化(serializable) 否 否 否 mysql默认的事务隔离级别为可重复读
-
读未提交
-
一个事务中,可以读取到其他事务未提交的变更
-
-
读已提交
-
一个事务中,可以读取到其他事务已经提交的变更
-
-
可重复读
-
一个事务中,直到事务结束前,都可以反复读取到事务刚开始看到的数据,不会发生变化
RR和RC的区别是在一个事务中RR隔离级别的读到一张表的数据都是一样
事务A 事务B begin; select * from a insert into a(...) select * from a RR隔离级别下:事务A二次select查询的结果是一样的,看不到事务B中插入的数据 RC隔离级别下:事务A第二次select查询是可以看到事务B中插入的数据
-
-
串行化
-
即便每次读都需要获得表级共享锁,每次写都加表级排它锁,两个会话间读写会相互阻塞。
-
-
补充:
-
事务隔离级别为串行化时,读写数据都会锁住整张表
-
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
-
-
⑨Mysql常用的数据库引擎
-
MyISAM
默认的存储引擎,提供高速存储和检索,以及全文搜索能力。 不支持事务。表级锁。不能在表损坏后恢复数据。 每个表会生成三个文件(文件名就是表名): .frm 表结构; .MYD 数据; .MYI 索引。 适合在以下几种情况下使用: 1.做很多count的计算 2.查询非常频繁
-
InnoDB
具有提交、回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎。 基于聚簇索引建立,聚簇索引对主键查询有很高的性能。不过它的二级索引(secondary index,非主键索引)中必须包含主键列,所以如果主键列很大的话,其他的所有索引都会很大。因此表上的索引较多的话,主键应当尽可能的小。 支持事务和外键。行级锁。 适合在以下几种情况下使用: 1.更新和查询都相当的频繁,多重并发 2.要求事务,或者可靠性要求比较高 3.外键约束,MySQL支持外键的存储引擎只有InnoDB 一般来说,如果需要事务支持,并且有较高的并发读取频率,InnoDB是不错的选择。
-
MyISAM和InnoDB两个引擎比较
-
innodb 支持事务,这一点非常重要,事务是一种高级的处理方式,如果在一些增删中如果那个出错可以换源回滚,myisam不支持事务
-
myisam适合查询以及插入为主的应用,Innodb适合频繁的修改以及设计到安全性能较高的应用
-
innodb适合外键,mysiam不适合外键
-
mysql中默认的mysiam引擎,如如果要是用innodb需要制定
-
InnoDB 中不保存表的行数,如 select count() from table 时,InnoDB;需要 扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。注意的是,当 count(*)语句包含 where 条件时 MyISAM 也需要扫描整个表;
-
对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM表中可以和其他字段一起建立联合索引;
-
清空整个表时,InnoDB 是一行一行的删除,效率非常慢。MyISAM 则会重建表;
-
InnoDB 支持行锁(某些情况下还是锁整表,如 update table set a=1 whereuser like ‘%lee%’)
-
⑩Mysql行锁与表锁
-
行锁
行锁就是一锁锁一行或者多行记录,mysql的行锁是基于索引加载的,所以行锁是要加在索引响应的行上,即命中索引,如下图所示:
如上图所示,数据库表中有一个主键索引和一个普通索引,Sql语句基于索引查询,命中两条记录。此时行锁一锁就锁定两条记录,当其他事务访问数据库同一张表时,被锁定的记录不能被访问,其他的记录都可以访问到。
行锁的特征:锁冲突概率低,并发性高,但是会有死锁的情况出现。
-
行锁表现
打开两个窗口,我们在窗口A中根据id更新一条记录,然后在窗口B中也执行相同的SQL语句看看
可以看到,窗口A先修改了id为3的用户信息后,还没有提交事务,此时窗口B再更新同一条记录,然后就提示Lock wait timeout exceeded; try restarting transaction ,由于窗口A迟迟没有提交事务,导致锁一直没有释放,就出现了锁冲突,而窗口B一直在等待锁,所以出现了超过锁定超时的警告了。
但是,此时我们如果去更新id为3它旁边的记录看看会出现怎样的情况,我们新打开一个窗口更新id为2的记录看看。
可以看到,在窗口B中更新id为3的记录报错,但是在窗口C中我们可以更新id为2的记录,这说明此时锁定了id为3的记录但是并没有锁定它旁边的记录。
-
-
表锁
表锁就是一锁锁一整张表,在表被锁定期间,其他事务不能对该表进行操作,必须等当前表的锁被释放后才能进行操作。表锁响应的是非索引字段,即全表扫描,全表扫描时锁定整张表,sql语句可以通过执行计划看出扫描了多少条记录。
由于表锁每次都是锁一整张表,所以表锁的锁冲突几率特别高,表锁不会出现死锁的情况。
和上面一样,我们通过代码演示一下,看看表锁的表现,我们打开两个窗口,在窗口A中更新一条记录,条件为非索引字段,不提交事务,然后在窗口B中任意再更新一条记录,我们看看会出现怎样的现象:
上面,我们分别验证了一下mysq的行锁和表锁,我们可以看到,当更新数据库数据时,如果没有触发索引,则会锁表,锁表后再对表做任何变更操作都会导致锁冲突,所以表锁的锁冲突概率较高。
⑩①乐观锁、悲观锁
-
悲观锁
当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【缩写“PCC”,又名“悲观锁”】。
悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改据)。
之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:
-
传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
-
Java 里面的同步 synchronized关键字的实现。
-
悲观锁主要分为共享锁和排他锁
-
共享锁【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
-
排他锁【exclusive locks】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
-
-
说明
-
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
-
-
-
乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。
乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:
-
CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
-
版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
-
说明
-
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
-
-
-
具体实现
-
悲观锁实现方式
悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
-
在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
-
如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
-
如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
-
期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
拿比较常用的 MySql Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。
要使用悲观锁,必须关闭 MySQL数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql语句:set autocommit=0)
-
以电商下单扣减库存的过程说明一下悲观锁的使用:
以上,在对id = 1的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。
如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得id=1的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。
上面提到,使用 select…for update会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。
-
-
乐观锁实现方式
乐观锁不需要借助数据库的锁机制。
主要就是两个步骤:冲突检测和数据更新。比较典型的就是 CAS (Compare and Swap)。
-
CAS
CAS 即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值(V)与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置(V)应该包含值(A)。如果包含该值,则将新值(B)放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。
当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。比如前面的扣减库存问题,通过乐观锁可以实现如下:
在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。
一个比较好的解决办法,就是通过一个单独的可以顺序递增的 version 字段。优化如下:
乐观锁每次在执行数据修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题。除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。
以上 SQL 其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度。一个比较好的建议,就是减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:
以上 SQL 语句中,如果用户下单数为 1,则通过
quantity - 1 > 0
的方式进行乐观锁控制。在执行过程中,会在一次原子操作中查询一遍 quantity 的值,并将其扣减掉 1。高并发环境下锁粒度把控是一门重要的学问。选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。
-
-
⑩②Mysql问题排查
-
使用 show processlist 命令查看当前所有连接信息
show processlist和show full processlist
processlist命令的输出结果显示了有哪些线程在运行,不仅可以查看当前所有的连接数,还可以查看当前的连接状态帮助识别出有问题的查询语句等。
如果是root帐号,能看到所有用户的当前连接。如果是其他普通帐号,则只能看到自己占用的连接。showprocesslist只能列出当前100条。如果想全部列出,可以使用SHOW FULL PROCESSLIST命令。
mysql> show processlist; +----+------+--------------------+------+---------+-------+-------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+--------------------+------+---------+-------+-------+------------------+ | 1 | root | localhost | NULL | Sleep | 12 | | NULL | | 2 | root | 192.168.100.1:7437 | test | Sleep | 8035 | | NULL | | 3 | root | 192.168.100.1:7438 | NULL | Sleep | 24348 | | NULL | | 5 | root | 192.168.100.1:7443 | NULL | Sleep | 24317 | | NULL | | 7 | root | 192.168.100.1:7450 | test | Sleep | 24272 | | NULL | | 9 | root | 192.168.100.1:5152 | test | Query | 0 | init | show processlist | +----+------+--------------------+------+---------+-------+-------+------------------+ 6 rows in set mysql> show full processlist; +----+------+--------------------+------+---------+-------+-------+-----------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+--------------------+------+---------+-------+-------+-----------------------+ | 1 | root | localhost | NULL | Sleep | 19 | | NULL | | 2 | root | 192.168.100.1:7437 | test | Sleep | 8042 | | NULL | | 3 | root | 192.168.100.1:7438 | NULL | Sleep | 24355 | | NULL | | 5 | root | 192.168.100.1:7443 | NULL | Sleep | 24324 | | NULL | | 7 | root | 192.168.100.1:7450 | test | Sleep | 24279 | | NULL | | 9 | root | 192.168.100.1:5152 | test | Query | 0 | init | show full processlist | +----+------+--------------------+------+---------+-------+-------+-----------------------+ 6 rows in set
各个列的含义:
①id列,用户登录mysql时,系统分配的"connection_id",可以使用函数connection_id()查看 ②user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句 ③host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户 ④db列,显示这个进程目前连接的是哪个数据库 ⑤command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等 ⑥time列,显示这个状态持续的时间,单位是秒 ⑦state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成 ⑧info列,显示这个sql语句,是判断问题语句的一个重要依据
在主从复制环境中,show processlist或show full processlist对于判断状态很有帮助,例如下面的state列:
-
使用 explain 命令查询 SQL 语句执行计划。
-
开启慢查询日志,查看慢查询的 SQL。
-
慢日志查询没有开启,可以开启一下。
mysql> show variables like 'slow_query_log'; +----------------+-------+ | Variable_name | Value | +----------------+-------+ | slow_query_log | OFF | +----------------+-------+ 1 row in set (0.00 sec)
-
打开慢日志查询命令:
set global slow_query_log=on
-
设置sql语句执行两秒钟以上就写到慢查询日志中:
set global long_query_time=2
设置完后要重新连接客户端才能看到设置后的long_query_time的值
-
查看慢查询sql的数量命令:
show status like '%slow_queries%'
-
测试一下慢查询日志,我们执行一个时间为三秒钟的sql:
-
然后到慢查询日志查看这条sql有没有被记录下来,发现已经在慢查询日志中:
-
⑩③Mysql性能优化
-
Mysql数据库的优化技术
-
表的设计合理化(符合3NF)
-
添加适当索引(index) [四种: 普通索引、主键索引、唯一索引unique、全文索引]
-
分表技术(水平分割、垂直分割)
-
读写[写: update/delete/add]分离
-
存储过程 [模块化编程,可以提高速度]
-
对mysql配置优化 [配置最大并发数my.ini, 调整缓存大小 ]
-
mysql服务器硬件升级
-
定时的去清除不需要的数据,定时进行碎片整理(MyISAM)
-
-
数据库优化工作
对于一个以数据为中心的应用,数据库的好坏直接影响到程序的性能,因此数据库性能至关重要。一般来说,要保证数据库的效率,要做好以下四个方面的工作:
① 数据库设计
② sql语句优化
③ 数据库参数配置
④ 恰当的硬件资源和操作系统
此外,使用适当的存储过程,也能提升性能。
这个顺序也表现了这四个工作对性能影响的大小
-
数据库表设计
-
通俗地理解三个范式,对于数据库设计大有好处。在数据库设计中,为了更好地应用三个范式,就必须通俗地理解三个范式(通俗地理解是够用的理解,并不是最科学最准确的理解):
第一范式:1NF是对属性的原子性约束,要求属性(列)具有原子性,不可再分解;(只要是关系型数据库都满足1NF)
第二范式:2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
第三范式:3NF是对字段冗余性的约束,它要求字段没有冗余。 没有冗余的数据库设计可以做到。
但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。具体做法是: 在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余。
-
-
SQL语句优化
-
SQL优化的一般步骤
-
通过show status命令了解各种SQL的执行频率。
-
定位执行效率较低的SQL语句-(重点select)
-
通过explain分析低效率的SQL
-
确定问题并采取相应的优化措施
-- select语句分类 Select Dml数据操作语言(insert update delete) dtl 数据事物语言(commit rollback savepoint) Ddl数据定义语言(create alter drop..) Dcl(数据控制语言) grant revoke -- Show status 常用命令 --查询本次会话 Show session status like 'com_%'; //show session status like 'Com_select' --查询全局 Show global status like 'com_%'; -- 给某个用户授权 grant all privileges on *.* to 'abc'@'%'; --为什么这样授权 'abc'表示用户名 '@' 表示host, 查看一下mysql->user表就知道了 --回收权限 revoke all on *.* from 'abc'@'%'; --刷新权限[也可以不写] flush privileges;
-
-
SQL语句优化-show参数
-
MySQL客户端连接成功后,通过使用show [session|global] status 命令可以提供服务器状态信息。其中的session来表示当前的连接的统计结果,global来表示自数据库上次启动至今的统计结果。默认是session级别的。 下面的例子: show status like 'Com_%'; 其中Com_XXX表示XXX语句所执行的次数。 重点注意:Com_select,Com_insert,Com_update,Com_delete通过这几个参数,可以容易地了解到当前数据库的应用是以插入更新为主还是以查询操作为主,以及各类的SQL大致的执行比例是多少。
还有几个常用的参数便于用户了解数据库的基本情况。 Connections:试图连接MySQL服务器的次数 Uptime:服务器工作的时间(单位秒) Slow_queries:慢查询的次数 (默认是慢查询时间10s)
show status like 'Connections' show status like 'Uptime' show status like 'Slow_queries'
如何查询mysql的慢查询时间
Show variables like 'long_query_time';
修改mysql 慢查询时间
set long_query_time=2
-
-
SQL语句优化-定位慢查询
问题是: 如何从一个大项目中,迅速的定位执行速度慢的语句. (定位慢查询)
首先我们了解mysql数据库的一些运行状态如何查询(比如想知道当前mysql运行的时间/一共执行了多少次select/update/delete.. / 当前连接)
为了便于测试,我们构建一个大表(400 万)-> 使用存储过程构建
默认情况下,mysql认为10秒才是一个慢查询.
修改mysql的慢查询。
show variables like 'long_query_time' ; //可以显示当前慢查询时间 set long_query_time=1 ;//可以修改慢查询时间
-
SQL语句优化-explain分析问题
Explain select * from emp where ename=“wsrcla” 会产生如下信息: select_type:表示查询的类型。 table:输出结果集的表 type:表示表的连接类型 possible_keys:表示查询时,可能使用的索引 key:表示实际使用的索引 key_len:索引字段的长度 rows:扫描出的行数(估算的行数) Extra:执行情况的描述和说明
Extra
查询细节信息
No tables :Query语句中使用FROM DUAL 或不含任何FROM子句
Using filesort :当Query中包含 ORDER BY 操作,而且无法利用索引完成排序,
Impossible WHERE noticed after reading const tables: MYSQL Query Optimizer
通过收集统计信息不可能存在结果
Using temporary:某些操作必须使用临时表,常见 GROUP BY ; ORDER BY
Using where:不用读取表中所有信息,仅通过索引就可以获取所需数据;
-
-
⑩④数据库的4种隔离级别及7种传播方式
第1级别:Read Uncommitted(读取未提交内容) (1)所有事务都可以看到其他未提交事务的执行结果 (2)本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少 (3)该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
第2级别:Read Committed(读取提交内容)
(1)这是大多数数据库系统的默认隔离级别(但不是MySQL默认的) (2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变 (3)这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。 |——>导致这种情况的原因可能有:(1)有一个交叉的事务有新的commit,导致了数据的改变;(2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
第3级别:Repeatable Read(可重读) (1)这是MySQL的默认事务隔离级别 (2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行 (3)此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行 (4)InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题
第4级别:Serializable(可串行化) (1)这是最高的隔离级别 (2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。 (3)在这个级别,可能导致大量的超时现象和锁竞争
传播行为
1、PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
2、PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘
3、PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
4、PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
5、PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6、PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
7、PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
⑩⑤Mysql索引方法
-
B-Tree
B-Tree是最常见的索引类型,所有值(被索引的列)都是排过序的,每个叶节点到跟节点距离相等。所以B-Tree适合用来查找某一范围内的数据,而且可以直接支持数据排序(ORDER BY) B-Tree在MyISAM里的形式和Innodb稍有不同: MyISAM表数据文件和索引文件是分离的,索引文件仅保存数据记录的磁盘地址 InnoDB表数据文件本身就是主索引,叶节点data域保存了完整的数据记录
-
Hash索引
1.仅支持"=","IN"和"<=>"精确查询,不能使用范围查询: 由于Hash索引比较的是进行Hash运算之后的Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的Hash算法处理之后的Hash 2.不支持排序: 由于Hash索引中存放的是经过Hash计算之后的Hash值,而且Hash值的大小关系并不一定和Hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算 3.在任何时候都不能避免表扫描: 由于Hash索引比较的是进行Hash运算之后的Hash值,所以即使取满足某个Hash键值的数据的记录条数,也无法从Hash索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果 4.检索效率高,索引的检索可以一次定位,不像B-Tree索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以Hash索引的查询效率要远高于B-Tree索引 5.只有Memory引擎支持显式的Hash索引,但是它的Hash是nonunique的,冲突太多时也会影响查找性能。Memory引擎默认的索引类型即是Hash索引,虽然它也支持B-Tree索引
⑩⑥Mysql优化
-
SQL语句优化
(1)使用limit对查询结果的记录进行限定 (2)避免select *,将需要查找的字段列出来 (3)使用连接(join)来代替子查询 (4)拆分大的delete或insert语句
-
选择合适的数据类型
(1)使用可存下数据的最小的数据类型,整型 < date,time < char,varchar < blob (2)使用简单的数据类型,整型比字符处理开销更小,因为字符串的比较更复杂。如,int类型存储时间类型,bigint类型转ip函数 (3)使用合理的字段属性长度,固定长度的表会更快。使用enum、char而不是varchar (4)尽可能使用not null定义字段 (5)尽量少用text,非用不可最好分表
-
选择合适的索引列
(1)查询频繁的列,在where,group by,order by,on从句中出现的列 (2)where条件中<,<=,=,>,>=,between,in,以及like 字符串+通配符(%)出现的列 (3)长度小的列,索引字段越小越好,因为数据库的存储单位是页,一页中能存下的数据越多越好 (4)离散度大(不同的值多)的列,放在联合索引前面。查看离散度,通过统计不同的列值来实现,count越大,离散程度越高:
mysql> SELECT COUNT(DISTINCT column_name) FROM table_name;
-
使用命令分析
1)SHOW查看状态 1.显示状态信息
mysql> SHOW [SESSION|GLOBAL] STATUS LIKE '%Status_name%';
session(默认):取出当前窗口的执行 global:从mysql启动到现在 (a)查看查询次数(插入次数com_insert、修改次数com_insert、删除次数com_delete)
mysql> SHOW STATUS LIKE 'com_select';
(b)查看连接数(登录次数)
mysql> SHOW STATUS LIKE 'connections';
(c)数据库运行时间
mysql> SHOW STATUS LIKE 'uptime';
(d)查看慢查询次数
mysql> SHOW STATUS LIKE 'slow_queries';
(e)查看索引使用的情况:
mysql> SHOW STATUS LIKE 'handler_read%';
handler_read_key:这个值越高越好,越高表示使用索引查询到的次数。 handler_read_rnd_next:这个值越高,说明查询低效。 2.显示系统变量
mysql> SHOW VARIABLES LIKE '%Variables_name%';
3.显示InnoDB存储引擎的状态
mysql> SHOW ENGINE INNODB STATUS;
(2)EXPLAIN分析查询
mysql> EXPLAIN SELECT column_name FROM table_name;
explain查询sql执行计划,各列含义: table:表名; type:连接的类型 -const:主键、索引; -eq_reg:主键、索引的范围查找; -ref:连接的查找(join) -range:索引的范围查找; -index:索引的扫描; -all:全表扫描; possible_keys:可能用到的索引; key:实际使用的索引; key_len:索引的长度,越短越好; ref:索引的哪一列被使用了,常数较好; rows:mysql认为必须检查的用来返回请求数据的行数; extra:using filesort、using temporary(常出现在使用order by时)时需要优化。 -Using filesort 额外排序。看到这个的时候,查询就需要优化了 -Using temporary 使用了临时表。看到这个的时候,也需要优化 (3)PROFILING分析SQL语句 1.开启profile。查看当前SQL执行时间
mysql> SET PROFILING=ON; mysql> SHOW profiles;
2.查看所有用户的当前连接。包括执行状态、是否锁表等
mysql> SHOW processlist;
(4)PROCEDURE ANALYSE()取得建议 通过分析select查询结果对现有的表的每一列给出优化的建议
mysql> SELECT column_name FROM table_name PROCEDURE ANALYSE();
(5)OPTIMIZE TABLE回收闲置的数据库空间
mysql> OPTIMIZE TABLE table_name;
对于MyISAM表,当表上的数据行被删除时,所占据的磁盘空间并没有立即被回收,使用命令后这些空间将被回收,并且对磁盘上的数据行进行重排(注意:是磁盘上,而非数据库)。 对于InnoDB表,OPTIMIZE TABLE被映射到ALTER TABLE上,这会重建表。重建操作能更新索引统计数据并释放成簇索引中的未使用的空间。 只需在批量删除数据行之后,或定期(每周一次或每月一次)进行一次数据表优化操作即可,只对那些特定的表运行。 (6)REPAIR TABLE修复被破坏的表
mysql> REPAIR TABLE table_name;
(7)CHECK TABLE检查表是否有错误
mysql> CHECK TABLE table_name;
⑩⑥Mysql分区和分表
1.为什么要分表和分区? 日常开发中我们经常会遇到大表的情况,所谓的大表是指存储了百万级乃至千万级条记录的表。这样的表过于庞大,导致数据库在查询和插入的时候耗时太长,性能低下,如果涉及联合查询的情况,性能会更加糟糕。分表和表分区的目的就是减少数据库的负担,提高数据库的效率,通常点来讲就是提高表的增删改查效率。 2.什么是分表? 分表是将一个大表按照一定的规则分解成多张具有独立存储空间的实体表,我们可以称为子表,每个表都对应三个文件,MYD数据文件,.MYI索引文件,.frm表结构文件。这些子表可以分布在同一块磁盘上,也可以在不同的机器上。app读写的时候根据事先定义好的规则得到对应的子表名,然后去操作它。 3.什么是分区? 分区和分表相似,都是按照规则分解表。不同在于分表将大表分解为若干个独立的实体表,而分区是将数据分段划分在多个位置存放,可以是同一块磁盘也可以在不同的机器。分区后,表面上还是一张表,但数据散列到多个位置了。app读写的时候操作的还是大表名字,db自动去组织分区的数据。 4.mysql分表和分区有什么联系呢? (1)都能提高mysql的性高,在高并发状态下都有一个良好的表现。 (2)分表和分区不矛盾,可以相互配合的,对于那些大访问量,并且表数据比较多的表,我们可以采取分表和分区结合的方式(如果merge这种分表方式,不能和分区配合的话,可以用其他的分表试),访问量不大,但是表数据很多的表,我们可以采取分区的方式等。 (3)分表技术是比较麻烦的,需要手动去创建子表,app服务端读写时候需要计算子表名。采用merge好一些,但也要创建子表和配置子表间的union关系。