开始重新学习mysql数据库,这个文章将记录我学习mysql时的笔记以及遇到的问题
mac如何启动mysql:
安装numactl
mkdir mysql
tar -xvf mysql-8.0.26-1.el7.x86_64.rpm-bundle.tar -C mysql
cd mysql
rpm -ivh mysql-community-common-8.0.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-plugins-8.0.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-8.0.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-compat-8.0.26-1.el7.x86_64.rpm
yum install openssl-devel
rpm -ivh mysql-community-devel-8.0.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-8.0.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-8.0.26-1.el7.x86_64.rpm
systemctl start mysqld
systemctl restart mysqld
systemctl stop mysqld
grep 'temporary password' /var/log/mysqld.log
命令行执行指令 :
mysql -u root -p
然后输入上述查询到的自动生成的密码, 完成登录 .
登录到MySQL之后,需要将自动生成的不便记忆的密码修改了,修改成自己熟悉的便于记忆的密码。
ALTER USER 'root'@'localhost' IDENTIFIED BY '1234';
执行上述的SQL会报错,原因是因为设置的密码太简单,密码复杂度不够。我们可以设置密码的复杂度为简单类型,密码长度为4。
set global validate_password.policy = 0;
set global validate_password.length = 4;
降低密码的校验规则之后,再次执行上述修改密码的指令。
默认的root用户只能当前节点localhost访问,是无法远程访问的,我们还需要创建一个root账户,用户远程访问
create user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '1234';
注意:当你创建了一个可以远程连接的root用户以后,和本地root用户是同一个,只是登陆密码是可以不一样的。
grant all on *.* to 'root'@'%';
mysql -u root -p
然后输入密码
sql注释:
SQL又分为4类
MySQL中的数据类型有很多,主要分为三类:
注意:实际代码不需要加中括号[],[]只是表示 可选
CREATE TABLE 表名(
字段1 字段1类型[COMMENT 字段1注释],
字段2 字段2类型[COMMENT 字段2注释],
字段3 字段3类型[COMMENT 宇段了注释],
...
字段n 字段n类型[COMMENT 字段n注释]
)[COMMENT 表注释];
注意 :[ ... ] 为可选参数, 最后一个字段后面没有逗号
添加字段:
alter table 表名 add 字段名 类型 [comment 注释] [约束];
修改:
表删除字段:
表名修改:
表删除:
编写顺序
SELECT
字段列表
FROM
表名字段
WHERE
条件列表
GROUP BY
分组字段列表
HAVING
分组后的条件列表
ORDER BY
排序字段列表 升序:asc, 降序desc
LIMIT
分页参数
select:查询哪些字段
语法:SELECT 字段列表 FROM 表名 WHERE 条件列表;
条件:
例子:
-- 年龄等于30
select * from employee where age = 30;
-- 年龄小于30
select * from employee where age < 30;
-- 小于等于
select * from employee where age <= 30;
-- 没有身份证
select * from employee where idcard is null or idcard = '';
-- 有身份证
select * from employee where idcard;
select * from employee where idcard is not null;
-- 不等于
select * from employee where age != 30;
-- 年龄在20到30之间
select * from employee where age between 20 and 30;
select * from employee where age >= 20 and age <= 30;
-- 下面语句不报错,但查不到任何信息
select * from employee where age between 30 and 20;
-- 性别为女且年龄小于30
select * from employee where age < 30 and gender = '女';
-- 年龄等于25或30或35
select * from employee where age = 25 or age = 30 or age = 35;
select * from employee where age in (25, 30, 35);
-- 姓名为两个字
select * from employee where name like '__';
-- 身份证最后为X
select * from employee where idcard like '%X';
SELECT 聚合函数(字段列表) FROM 表名;
例:
SELECT count(id) from employee where workaddress = "广东省";
注意:null不参与聚合函数的运算
语法:
SELECT 字段列表 FROM 表名 [ WHERE 条件 ] GROUP BY 分组字段名 [ HAVING 分组后的过滤条件 ];
分组之前的过滤用where,分组之后的过滤用having。
where 和 having 的区别:
例子:
-- 根据性别分组,统计男性和女性数量(只显示分组数量,不显示哪个是男哪个是女)
select count(*) from employee group by gender;
-- 根据性别分组,统计男性和女性数量
select gender, count(*) from employee group by gender;
-- 根据性别分组,统计男性和女性的平均年龄
select gender, avg(age) from employee group by gender;
-- 年龄小于45,并根据工作地址分组
select workaddress, count(*) from employee where age < 45 group by workaddress;
-- 年龄小于45,并根据工作地址分组,获取员工数量大于等于3的工作地址
select workaddress, count(*) address_count from employee where age < 45 group by workaddress having address_count >= 3;
语法:
SELECT 字段列表 FROM 表名 ORDER BY 字段1 排序方式1, 字段 2 排序方式2;
排序方式:
例子:
-- 根据年龄升序排序
SELECT * FROM employee ORDER BY age ASC;
SELECT * FROM employee ORDER BY age;
-- 两字段排序,根据年龄升序排序,入职时间降序排序
SELECT * FROM employee ORDER BY age ASC, entrydate DESC;
注意事项
如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序
语法:
SELECT 字段列表 FROM 表名 LIMIT 起始索引, 查询记录数;
例子:
-- 查询第一页数据,展示10条
SELECT * FROM employee LIMIT 0, 10;
-- 查询第二页
SELECT * FROM employee LIMIT 10, 10;
注意事项
查询用户:
USE mysql;
SELECT * FROM user;
创建用户:
CREATE USER '用户名'@'主机名' identified by '密码';
修改用户密码:
alter user '用户名'@'主机名' identified WITH mysql_native_password BY '新密码';
删除用户:
DROP USER '用户名'@'主机名';
例子:
-- 创建用户test,只能在当前主机localhost访问
create user 'test'@'localhost' identified by '123456';
-- 创建用户test,能在任意主机访问
create user 'test'@'%' identified by '123456';
-- 修改密码
alter user 'test'@'localhost' identified with mysql_native_password by '1234';
-- 删除用户
drop user 'test'@'localhost';
注意事项
show grants FOR '用户名'@'主机名';
授予权限:
grant 权限列表 ON 数据库名.表名 TO '用户名'@'主机名';
撤销权限:
revoke 权限列表 ON 数据库名.表名 FROM '用户名'@'主机名';
注意事项
-- 拼接
SELECT CONCAT('Hello', 'World');
-- 小写
SELECT LOWER('Hello');
-- 大写
SELECT UPPER('Hello');
-- 左填充
SELECT LPAD('01', 5, '-');
-- 右填充
SELECT RPAD('01', 5, '-');
-- 去除空格
SELECT TRIM(' Hello World ');
-- 切片(起始索引为1)
SELECT SUBSTRING('Hello World', 1, 5);
--生成六位随机验证码
select lpad(ceil(rand()*1000000),6,0);
-- DATE_ADD
SELECT DATE_ADD(NOW(), INTERVAL 70 YEAR);
select
name,
(case when age > 30 then '中年' else '青年' end)
from employee;
select
name,
(case workaddress when '北京市' then '一线城市' when '上海市' then '一线城市' else '二线城市' end) as '工作地址'
from employee;
概念:约束是作用于表中字段上的规则,用于限制存储在表中的数据。
目的:保证数据库中数据的正确、有效性和完整性。
注意: 约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束。
例子:
create table user(
id int primary key auto_increment,
name varchar(10) not null unique,
age int check(age > 0 and age < 120),
status char(1) default '1',
gender char(1)
);
概念 :外键用来让两张表的数据之间建立连接,从而保证数据的一致性和完整性。
例如,如下图所示
emp表的最后一个字段关联了另一张表的主键,则最后一个字段称为外键。通过外键让两张表数据产生连接。
语法:
--创建时设置
CREATE TABLE 表名(
字段名 字段类型,
...
[constraint] [外键名称] foreign key(外键字段名) references 主表(主表列名)
);
--修改
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(主表列名);
--删除
ALTER TABLE 表名 DROP FOREIGN KEY 外键名;
例子:
alter table emp add constraint fk_emp_dept_id foreign key(dept_id) references dept(id);
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(主表列名) on update 行为 on delete 行为;
多表关系:
一对多:
多对多:
一对一:
直接合并查询,会产生笛卡尔积,即会展示所有组合结果:
select * from employee, dept;
如下图所示:
因此,在多表查询时,需要消除无效的笛卡尔积
消除无效笛卡尔积:只需要让子表中的外键=父表中的主键即可
select * from employee, dept where employee.dept = dept.id;
多表查询的分类
内连接查询的是两张表交集的部分
隐式内连接:
SELECT 字段列表 FROM 表1, 表2 WHERE 条件 …;
显式内连接:
SELECT 字段列表 FROM 表1 inner join 表2 on 连接条件 …; //inner可省略
显式性能比隐式高
-- 查询员工姓名,及关联的部门的名称
-- 隐式
select e.name, d.name from employee as e, dept as d where e.dept = d.id;
-- 显式
select e.name, d.name from employee as e inner join dept as d on e.dept = d.id where e.age < 30;
左外连接:
查询左表所有数据,以及两张表交集部分数据
SELECT 字段列表 FROM 表1 left outer join 表2 on 条件 …; //outer可省略
相当于查询表1的所有数据,包含表1和表2交集部分数据
在实际开发中,右外也可以改成左外,所以只用了解左外即可。
子连接:
自身的表与自身的表连接查询
当前表与自身的连接查询,自连接必须使用表别名
自连接查询,可以是内连接查询,也可以是外连接查询
语法:
例子:
-- 查询员工及其所属领导的名字
select a.name, b.name from employee a, employee b where a.manager = b.id;
-- 没有领导的也查询出来
select a.name, b.name from employee a left join employee b on a.manager = b.id;
把多次查询的结果合并,形成一个新的查询集
语法:
SELECT 字段列表 FROM 表A …
union [all]
SELECT 字段列表 FROM 表B …
注意事项
子查询:
SQL语句中嵌套SELECT语句,那个被嵌套的select的查询称为嵌套查询,又称子查询。
SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2);
子查询外部的语句可以是 insert / update / delete / select /where/ from的任何一个
根据子查询结果可以分为:
根据子查询位置可分为:
子查询返回的结果是单个值(数字、字符串、日期等)。
其实标量子查询就是编程的思想,把这个查询的语句当作返回的值直接当结果输入到另一个查询的语句中使用。
常用操作符:- < > > >= < <=
-- 查询销售部所有员工
select id from dept where name = '销售部';
-- 根据销售部部门ID,查询员工信息
select * from employee where dept = 4;
-- 合并(子查询)
select * from employee where dept = (select id from dept where name = '销售部');
-- 查询xxx入职之后的员工信息
select * from employee where entrydate > (select entrydate from employee where name = 'xxx');
子查询返回的结果是一列(可以是多行)。
例子:
-- 查询销售部和市场部的所有员工信息
select * from employee where dept in (select id from dept where name = '销售部' or name = '市场部');
-- 查询比财务部所有人工资都高的员工信息
select * from employee where salary > all(select salary from employee where dept = (select id from dept where name = '财务部'));
-- 查询比研发部任意一人工资高的员工信息
select * from employee where salary > any (select salary from employee where dept = (select id from dept where name = '研发部'));
返回的结果是一行(可以是多列)。
常用操作符:=, <, >, IN, NOT IN
例子:
-- 查询与xxx的薪资及直属领导相同的员工信息
select * from employee where (salary, manager) = (12500, 1);
select * from employee where (salary, manager) = (select salary, manager from employee where name = 'xxx');
返回的结果是多行多列
常用操作符:IN
例子:
-- 查询与xxx1,xxx2的职位和薪资相同的员工
select * from employee where (job, salary) in (select job, salary from employee where name = 'xxx1' or name = 'xxx2');
-- 查询入职日期是2006-01-01之后的员工,及其部门信息
select e.*, d.* from (select * from employee where entrydate > '2006-01-01') as e left join dept as d on e.dept = d.id;
事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么一起成功,要么一起失败。
想象一个例子:
张三要转账给李四,在数据库来说有三个操作:
上述三个操作需要看作一个事务,不然就错了。
利用事务的操作方式一:
-- 查看事务提交方式
SELECT @@AUTOCOMMIT;
-- 设置事务提交方式,1为自动提交,0为手动提交,该设置只对当前会话有效
SET @@AUTOCOMMIT = 0;
-- 提交事务
commit;
-- 回滚事务
rollback;
操作实例:
-- 设置手动提交
SELECT @@AUTOCOMMIT;
SET @@AUTOCOMMIT = 1;
-- 操作数据
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
-- 提交事务:将数据真正提交到数据库
commit;
如果设置成手动提交事务的话,执行sql语句后,数据不会更新到数据库本身,只有输入commit才会真正更新数据库
利用事务的操作方式二:
开启事务:
start transaction 或 begin transaction;
提交事务:
commit;
回滚事务:
rollback;
操作实例:
-- 开启事务
start transaction;
-- 操作数据
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
update account set money = money + 1000 where name = '李四';
-- 提交事务
commit;
四大特性ACID:
多个并发事务会产生一系列问题,最常见的是如下三个:
上述三个并发事务的问题可以通过事务的隔离级别来解决
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted(读未提交) | 存在 | 存在 | 存在 |
Read committed(读提交) | 不存在 | 存在 | 存在 |
Repeatable Read(默认)(可重复读) | 不存在 | 不存在 | 存在 |
Serializable(串行化) | 不存在 | 不存在 | 不存在 |
从上到下数据的安全性越来越高,但是性能越来越差。
查看事务隔离级别:
SELECT @@transaction_isolation;
设置事务隔离级别:
set [ session | global ] transaction isolation level {Read uncommitted | Read committed | Repeatable Read | Serializable };
SESSION 是会话级别,表示只针对当前会话有效,GLOBAL 表示对所有会话有效
MySQL体系结构:
如下图所示:
存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表而不是基于库的,所以存储引擎也可以被称为表引擎。
注意:索引是在存储引擎层实现的,所以不同的存储引擎的索引结构是不同的。
建表时如何指定存储引擎:
-- 建表时指定存储引擎
CREATE TABLE 表名(
...
) ENGINE=INNODB;
innoDB 是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB 是默认的 MySQL 引擎。
特点:
文件形式:
存储引擎特点:
在innodb中,page页是磁盘操作的最小单元。
mysql早期的默认存储引擎。
特点:
文件:
Memory 引擎的表数据是存储在内存中的,受硬件问题、断电问题的影响,只能将这些表作为临时表或缓存使用。
特点:
文件:
在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据实际情况选择多种存储引擎进行组合。
索引是帮助 MySQL 高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查询算法,这种数据结构就是索引。
优缺点:
索引是在第三层存储引擎层实现的,所以存储引擎的不同,索引也不同。
索引在不同存储引擎的支持情况:
一般索引都默认是B+树索引,后文如果没有特意强调,都指的是B+树索引。
就是二叉查找树的多叉版。
注意:B树的分叉是在节点的左右两边的。B树的叶子节点是最下面的空节点
那么如何保证m阶的B树的查找效率呢?(如何维护B树,B树的条件)
B+树是应对数据库所需而出现的一种B树的变形树。
B树和B+树的区别:
一棵m阶的B+树的条件:
B+树有两种查找方式:
为什么B+树更适合存储数据?
首先明确,在B/B+树中,一个节点存放在一个磁盘块中。在B树中,非叶节点存放了该关键字对应的存储地址,而在B+树中,只有叶子节点才会从存放关键字对应的存储地址,所以可以使一个磁盘块可以包含更多的关键字,使得B+树的阶更大,树更矮,读取磁盘的次数更少,查找更快。
那为什么不使用红黑树或者平衡二叉树来存? 因为相比于二叉树,B+树的高度更低,搜索效率高
注意:数据库的B+树的叶子节点是双向链表(原版是单向链表)
在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:
为什么二级索引不存储行数据?
因为如果二级索引也存行数据,那就太冗余了。所以二级索引存的是主键的值。
由于聚集索引必须要有,且只能有一个,所以聚集索引存在一个选取规则:
聚集索引和二级索引如下图所示:
注意:聚集索引和二级索引对应的叶子节点存放的内容需要记住-----聚集索引的叶子节点存放的具体的主键对应的每一行的数据而二级索引存放的是主键的值。这是sql优化的关键!
select * from user where id = 10;
select * from user where name = 'Arm';
-- 备注:id为主键,name字段创建的有索引
答:第一条语句,因为第二条需要回表查询----先查询二级索引,在通过二级索引的结果得知主键,再根据主键去查询聚集索引得到表的内容。
答:假设一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB 的指针占用6个字节的空间,主键假设为bigint,占用字节数为8.
设n为key的数量:
可得公式:n * 8 + (n + 1) * 6 = 16 * 1024
其中 8 表示 bigint 占用的字节数,n 表示当前节点存储的key的数量,(n + 1) 表示指针数量(比key多一个)。算出n约为1170。
如果树的高度为2,那么他能存储的数据量大概为:1171 * 16 = 18736;
如果树的高度为3,那么他能存储的数据量大概为:1171 * 1171 * 16 = 21939856。
另外,如果有成千上万的数据,那么就要考虑分表,涉及运维篇知识。
创建索引:
CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (index_col_name, ...);
查看索引:
SHOW INDEX FROM table_name;
删除索引:
DROP INDEX index_name ON table_name;
查询增删改查的执行频次
SHOW GLOBAL/SESSION STATUS LIKE 'Com_______'; --7个下划线,模糊匹配
-- 看全局或者看当前会话的
事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。
为什么需要redolog?
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
注意:Write-Ahead Log(预先日志持久化):先写日志,再写磁盘。并且,在事务commit以后,redo buffer才会开始根据策略提交缓存中的数据到redo log(磁盘)。
分析:其实只要第三步,能将redo buffer刷新到磁盘的日志文件中,那么一定可以保证数据的持久化,所以需要研究下redo buffer是如何刷新到磁盘中的。
首先明确:InnoDB引擎并不是直接将命令写到redo log磁盘中的,是先同步到redo log buffer,然后事务提交后,在通过一定的策略,将redo log buffer内容刷新到redo log(磁盘)中。
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。
那么策略是什么呢,以及把写入交给系统也会存在问题,如何解决呢?针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit
参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
注意:InnoDB引擎有个后台线程,每隔1s,自动将redo buffer中的内容写到page cache,并调用刷盘操作
上图是参数为1的流程图,其他参数的类似,先看下面的分析,再来看上图的流程图。
分析每个参数的策略(注意,这一块有点像redis的AOF持久化部分,问题也是aof缓存什么时候将内容写到aof文件中,P140)
如何写入redo buffer:
Mini-Transaction(mtr):
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo日志,画个图表示它们的关系就是这样:
不同的事务可能是并发执行的,所以事务 T1 、 事务T2 之间的 mtr 可能是交替执行的。所以存入redo buffer中,同一个事务的不同mtr可能不会存在一块,但是一个属于同一个mtr的redo日志一定存在同一块。如下图所示
如何写入redo log:
日志中有两个关键的属性:
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个 能力称为crash-safe。
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中更新数据的前置操作 其实是要 先写入一个 undo log 。 如下图所示
事务需要保证原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
以上情况出现,我们需要把数据改回原先的样子,这时候就需要undo log帮忙,这个过程称之为 回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。
注意:undo可以将数据回滚到执行语句/事务之前的样子。但是只是逻辑上回复到原来的样子,物理上页可能会发生变化,这没办法变回去(不理解这句话就跳过,无所谓。)
总结undo 日志的作用:
当事务提交时,InnoDB存储引擎会做以下两件事情:
事务的隔离性由 锁机制 实现。
同时 锁机制 也为实现MySQL 的各个隔离级别提供了保证。 锁冲突 也是影响数据库 并发访问性能的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
假设两个并发事务对同一个记录进行操作,会分为三种情况
读-读 情况,即并发事务相继 读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。通过锁机制来解决脏写问题。具体来说:所以在多个未提交事务 相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。这个所谓的锁其实是一个内存中的结构 ,在事务执行前记录本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的,如图所示:
当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构 ,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个锁结构与T1关联:
当又有一个事务T2想要操作这条记录时,又会生成一个锁结构与T2关联:
读-写 或 写-读 ,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、不可重复读 、幻读 的问题。
两种方案解决脏读 、不可重复读 、幻读这些问题
小结对比发现:
一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况 下,要求必须采用加锁的方式执行。下面就讲解下MySQL中不同类别的锁。
例如(行级读写锁):如果一个事务T1 已经获得了某个行r的读锁,那么此时另外的一个事务 T2 是可以去获得这个行r的读锁的,因为读取探作井没有改变行r的数据;但是,如果某个事务 T3 想获得行r的写锁,则它必须等待事务T1、T2 释放掉行r上的读锁才行。
总结:S锁遇到S锁不会阻塞,X锁(序号1)遇到S锁或者X锁(序号2),当前X锁(序号1)就会阻塞,直到其他S锁或者X锁提交事务。
如何加锁?
SELECT ... LOCK IN share mode; //加共享锁
SELECT ... for share;//加共享锁
SELECT ... for update; //加排他锁(是的,读锁也能加排他锁)
mysql8.0新特性
:在select … for update, select … for share后添加nowait
、skip locked
语法,可以跳过锁等待。
具体来说:
从数据操作粒度划分,可将锁划分为:
为了提高数据库的并发度,每次锁的数据范围越小越好,但是管理锁又是一件很消耗资源的操作。因此,数据库需要在高并发与系统性能之间做平衡,这样就产生了锁粒度
的概念
当只对一条记录加锁时,我们可以称锁的粒度比较细。当对整张表加锁时,可以称锁的粒度比较粗。
表锁会锁住整个表,他是Mysql中最基本的锁策略,并不依赖于存储引擎(什么存储引擎对于表锁的策略都是一样的)。表锁开销最小(粒度大),但是并发性差。由于表锁一次锁住整张表,可以避免死锁。
MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)
由于Innodb实现了粒度更细的行锁,所以Innodb尽量不使用表锁。只有MyISAM才会用表锁。
命令:
lock tables 表名 read/write;
unlock tables;
从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想 。
悲观锁:悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当 其他线程想要访问数据时,都需要阻塞挂起。
乐观锁:
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用 版本号机制 或者 CAS机制 实现。 乐观锁适用于多读的应用类型, 这样可以提高吞吐量。
两种锁的适用场景:
MVCC依赖于:隐藏字段,undo log,read view(等学完就懂为什么依赖这三个了)
MVCC (Multiversion Concurrency Control)多版本并发控制:顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制 。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
注意。上述有一句话:多个版本管理来实现数据库的并发控制。多个版本通过undo log实现,管理通过read view(视图)实现
使用MVCC主要就是为了解决上一章说过的一个场景,一个事务在写A数据,另一个事务在读A数据,如何解决这个冲突问题(会产生脏读,不可重复读,幻读的现象)?
所以,在我看来,MVCC和加锁,是两种解决两个事务同时读-写操作的方法。MVCC更加灵活,可以防止加锁带来的性能损失。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读-写冲突时,也能做到不加锁 , 非阻塞并发读 ,而这个读指的就是快照读 , 而非当前读 。当前 读实际上是写操作的时候读,读的是记录的最新版本。
MYSQL中,默认的隔离级别是可重复读。从定义来看,可重复读解决了脏读和不可重复读的问题。但是其实,可重复读通过MVCC的方法一并解决了幻读的问题。
在MVCC机制中,多个事务对同一个行记录进行操作时,会产生多个历史快照,这些历史快照保存在undo log中,如果一个事务想要查询这个行记录,系统需要知道他要读取哪个版本的的行记录,那么就需要用到readview进行管理。
Readview就是事务A在使用MVCC机制进行读操作时,产生的一个读视图。
在使用mysql的库的时候 需要使用链接库:
gcc hello.c -o hello -I/usr/include/mysql/ -L/usr/lib64/mysql/ -lmysqlclient
总体使用的函数有:
a. 初始化:MYSQL *mysql_init(MYSQL *mysql)
b. 错误处理 :unsigned int mysql_errno(MYSQL *mysql)
char *mysql_error(MYSQL *mysql);
c. 建立连接:
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char
*passwd,const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag);
d. 执行SQL语句:int mysql_query(MYSQL *mysql, const char *stmt_str)
e. 获取结果: MYSQL_RES *mysql_store_result(MYSQL *mysql)
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
f. 释放内存: void mysql_free_result(MYSQL_RES *result)
MYSQL *mysql_init(MYSQL *mysql)
MYSQL * mysql = mysql_init(NULL);
if(mysql == NULL)
{
printf("mysql init error \n");
return -1;
}
printf("mysql init ok\n");
//参数依次为 mysql通道,
//ip,用户名(如用户名为主机,则填"localhost"),
//密码,数据库名,
//端口号,socket(设置为NULL的时候表示不使用socket),客户端标识符(一般设置为0)
//mysql_real_connect函数若返回NULL表示则表示连接失败
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd
, const char *db, unsigned int port, const char *unix_socket, un signed long client_flag);
//初始化
MYSQL * mysql = mysql_init(NULL);
if(mysql == NULL)
{
printf("mysql init error ! \n");
return -1;
}
//连接数据库
MYSQL * conn = mysql_real_connect(mysql, "120.25.144.39", "root", "1234", "heima", 0, NULL, 0);
if(conn == NULL)
{
printf("mysql connect error ! \n");
return -1;
}
printf("connect mysql OK! \n");
mysql_close(conn);
printf("mysql close!\n");
mysql_query:只要mysql可以执行的sql语句,都可以用mysql_query执行。
当连接处于活动状态时,可以使用C语言 mysql_query()或mysql_real_query() 向服务器发出SQL查询。两者的差别在于,mysql_query()预期的查询为指定的、由Null终结的字符串,而mysql_real_query()预期的是计数字符串。
int mysql_query(MYSQL *mysql, const char *query)
int mysql_real_query(MYSQL *mysql, const char *query, unsigned long length)
//执行sql语句
int ret = mysql_query(conn, "insert into student values (11, '小路')");
if( ret != 0)
{
printf("mysql_query 失败!\n");
return -1;
}
printf("mysql_query 成功!\n");
获取结果集就是对数据库进行SELECT、SHOW等获取操作。
一种方式是通过 mysql_store_result() 将整个结果集全部取回来。另一种方式则是调用 mysql_use_result() 初始化获取操作,但暂时不取回任何记录。视结果集的条目数选择获取结果集的函数。两种方法均通过 mysql_fetch_row() 来访问每一条记录。
函数原型:
返回值:
整体获取的结果集,保存在 MYSQL_RES 结构体指针中,通过检查mysql_store_result()是否返回NULL,可检测函数执行是否成功:
MYSQL_RES *result = mysql_store_result(mysql);
if (result == NULL)
{
ret = mysql_errno(mysql);
printf("查询失败%s\n", mysql_error(mysql));
return ret;
}
该函数调用成功,则SQL查询的结果被保存在result中,但我们不清楚有多少条数据。
获取列数、获取行数:
一个函数获取行数,两个函数具备获取列数的功能:
//获取行的个数
int rows = mysql_num_rows(result);//一般用不到
//获取结果集中字段的个数
int fields = mysql_num_fields(result);
接着使用mysql_fetch_row的方式将结果集中的数据逐条取出:
函数原型:
返回值:
获取表头信息:
获取表头的API函数有两个:
示例:
//执行select查询语句
int ret = mysql_query(mysql, "select * from student")
if( ret != 0)
{
printf("mysql_query 失败!\n");
return -1;
}
printf("mysql_query 成功!\n");
//获取结果集
MYSQL_RES *results = mysql_store_result(conn);
if(results==NULL)
{
printf("mysql_store_result error, [%s]\n",mysql_error(mysql));
return -1;
}
printf("mysql_store_result ok\n");
//获取列数
unsigned int row_num = mysql_num_fields(result);
//获取所有的字段名
MYSQL_FIELD *fields = NULL;
fields = mysql_fetch_fields(result); //得到表头的结构体数组
//打印表头
for(int i=0; i<row_num; ++i)
{
cout<<fields[i].row_num<<"\t";
}
//获取结果集中每一行记录
MYSQL_ROW row;
while( row=mysql_fetch_row(results) )
{
for(i=0; i<row_num; i++)
{
printf("%s\t", row[i]);
}
printf("\n");
}
释放结果集:
结果集处理完成,应调用对应的函数释放所占用的内存。
函数原型:
返回值:
mysql_free_result(result);
想使用mysql, 需要先安装libmysqlclien,然后必须链接上libmysqlclient.a的库和mysql.h的头文件。通过调用命令find查询库文件和头文件的地址。
mysql客户端编写思路分析:
1 mysql初始化--mysql_init
2 连接mysql数据库---mysql_real_connect
3 while(1)
{
//打印提示符:write(STDOUT_FILENO, "mysql >", strlen("mysql >"));
//读取用户输入: read(STDIN_FILENO, buf, sizeof(buf))
//判断用户输入的是否为退出: QUIT quit exit EXIT
if(strncasecmp(buf, "exit", 4)==0 || strncasecmp(buf, "quit", 4)==0)
{
//关闭连接---mysql_close();
exit();
}
//执行sql语句--mysql_query();
//若不是select查询, 打印执行sql语句影响的行数--mysql_affected_rows();
if(strncasecmp(buf, "select", 6)!=0)
{
printf("Query OK, %d row affected", mysql_affected_rows());
continue;
}
//若是select查询的情况
---//获取列数: mysql_field_count()
//获取结果集: mysql_store_result()
--获取列数: int mysql_num_fields();
//获取表头信息并打印表头信息:mysql_fetch_fields();
//循环获取每一行记录并打印: mysql_fetch_row()
//释放结果集: mysql_free_result()
}
4 关闭连接: mysql_close();
需求:在C++下,利用mysql API函数,实现一个类mysql的客户端窗口,实现对mysql数据库的操作
//C++实现mysql客户端开发
#include
#include
#include
#include
using namespace std;
class Mysql
{
public:
//初始化数据库,连接数据库
Mysql(char* ip, char* user, char* password, char* db, int port);
//增删改操作----update\insert\delete
void db_CUD(char * buf);
//查操作----select
void db_select(char * buf);
//判断是什么语句,然后自动调用相应的操作
void execute_sql(char * buf);
//断开数据库的连接
void db_close();
MYSQL * m_mysql;
MYSQL * m_conn;
MYSQL_RES * m_result;
};
//初始化数据库,连接数据库
Mysql::Mysql(char* ip, char* user, char* password, char* db, int port)
{
//初始化数据库
this->m_mysql = mysql_init(NULL);
if(this->m_mysql == NULL)
{
printf("mysql init error \n");
exit(-1);
}
//连接数据库
this->m_conn = mysql_real_connect(this->m_mysql, ip, user, password, db, port, NULL, 0);
if(this->m_conn==NULL)
{
printf("mysql connect error ! \n");
exit(-1);
}
//设置字符集----解决中文问题
cout<<"before:"<<mysql_character_set_name(this->m_conn)<<endl;
mysql_set_character_set(this->m_conn,"utf8");//设置字符集为 utf8
cout<<"after:"<<mysql_character_set_name(this->m_conn)<<endl;
}
//增删改操作----update\insert\delete
void Mysql::db_CUD(char * buf)
{
int ret = mysql_query(this->m_conn, buf);
//通过调用mysql_affected_rows(),可发现有多少行已被改变(影响)。
if( ret == 0)//mysql_query调用成功
{
ret = mysql_affected_rows(this->m_conn);
cout<<"Query OK, "<< ret <<" rows affected"<<endl;
cout<<"Rows matched: "<<ret<<" Changed: "<<ret<<" Warnings: 0"<<endl;
}
else//mysql_query调用失败---sql语句出错
cout<<"ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1"<<endl;
}
//查操作----select
void Mysql::db_select(char *buf)
{
int ret = mysql_query(this->m_conn, buf);
if(ret != 0)
cout<<"mysql_query--select失败!"<<endl;
else
{
//获取数据集
this->m_result = mysql_store_result(this->m_conn);
//获取失败
if( this->m_result == NULL)
{
cout<<"mysql_store_result error";
return;
}
//获取成功
else
{
//获取列数
int row_num = mysql_num_fields(this->m_result);
//打印表头信息
MYSQL_FIELD *fields = NULL;
fields = mysql_fetch_fields(this->m_result); //得到表头的结构体数组
cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;
for(int i=0; i<row_num; ++i)
{
cout<<fields[i].name<<"\t";
}
cout<<endl;
cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;
//获取结果集每一行记录
MYSQL_ROW row;
while( row = mysql_fetch_row(this->m_result) )
{
for (int i=0; i<row_num; i++)
{
cout<<row[i]<<"\t";
}
cout<<endl;
}
cout<<"+----+-----------+------+--------------------+--------+------------+-----------+---------+"<<endl;
//释放结果集
mysql_free_result(this->m_result);
}
}
}
//判断是什么语句,然后自动调用相应的操作
void Mysql::execute_sql(char * buf)
{
//先判断是不是QUIT quit EXIT exit
if(strncasecmp(buf, "quit", 4)==0 || strncasecmp(buf, "exit", 4) == 0)
{
this->db_close();
}
//在判断是增删改还是查
else if( strncasecmp(buf, "insert", 6) == 0 || strncasecmp(buf, "update", 6) == 0 || strncasecmp(buf, "delete", 6) == 0)
{
db_CUD(buf);
}
else if(strncasecmp(buf, "select", 6) == 0)
{
db_select(buf);
}
//输入错误指令
else
{
cout<<"ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near"<<"\'"<<buf<<"\'at line 1"<<endl;
}
}
//关闭连接
void Mysql::db_close()
{
mysql_close(this->m_conn);
cout<<"Bye"<<endl;
exit(1);
}
//处理buf异常情况
int clean_buf(char * buf)
{
//处理输入回车报错的情况
if(buf[0]=='\n')
return 1;
//去掉末尾的;
char * p = strrchr(buf, ';');
if(p!=NULL)
*p = 0x00;
//去除前面的空格
int i;
for(i=0; strlen(buf);++i)
{
if(buf[i]!=' ')//只要前面是空格,指针往前走
break;
}
int n = strlen(buf);
memmove(buf, buf+i, n-i+1);//memmove拷贝字符串。+1是因为多拷贝一个\0
return 0;
}
int main()
{
char buf[1024];
//初始化、连接数据库
Mysql mysql=Mysql("localhost", "root", "password", "da tabasename", 0);
while(1)
{
//将"mysql"输出到终端
write(STDIN_FILENO,"mysql> ",strlen("mysql> "));
//获取用户输入
memset(buf,0x00,sizeof(buf));
read(STDOUT_FILENO, buf, sizeof(buf));
//处理buf异常情况
int flag = clean_buf(buf);
if(flag==1)
continue;
//判断是什么语句,然后自动执行相应的语句
mysql.execute_sql(buf);
}
}