「新生代农民工」之前端工程师必会的数据库-MySQL(下)

910 阅读21分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

点击查看上一篇 「新生代农民工」之前端工程师必会的数据库-MySQL(上)

第五章 MySQL中的视图

视图(view)是一种虚拟存在的表, 是一个逻辑表, 本身不包含数据.
他是作为一个select语句保存在字典中.
我们称用来创建视图的表叫做「 基表 」.
使用视图也有很多优点, 比如我们只关心过程, 而不用去关注表; 我们还可以通过视图对某行某列进行权限管理.
同时同我们修改原表的时候, 不会对视图产生影响.

为什么在实际web应用中我们很少用到view

我们在web 开发中, 一般都会去使用ORM, 这样其实我们就不用每个数据库都去写一个视图查询.
每当我们去创建视图的时候都会去执行查询「基表」的操作, 这样就会出现很多额外的开销. 所以可以尽量少使用.
以前有写过很复杂的存储过程, 结果被公司的DBA一顿diao.
但是不用不代表我们不用, 「有剑不用」和「没有剑」那是两码事.

一、创建视图

我们创建视图的语法和创建表的很像, 只是我们需要定义「基表」.

-- 基于students表创建视图
create view v_students (id, name, age, sex) as select id,name,age,sex from students with check option;
desc v_students;
-- 得到
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int         | NO   |     | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
| age   | int         | YES  |     | NULL    |       |
| sex   | varchar(10) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
-- 查询视图数据
select * from v_students;
-- 得到
+----+--------+------+------+
| id | name   | age  | sex  |
+----+--------+------+------+
|  1 | 超哥   |   20 ||
|  2 | 孟洋   |   10 ||
|  3 | 梦佳   |   18 ||
|  4 | 爱德   |   18 ||
|  5 | 旭神   |   88 ||
+----+--------+------+------+

我们可以看到, 其实视图和我们的普通表没有什么区别;

创建视图的时候, 数据类型也是根据「基表」来的.

使用show tables;语句, 可以看到v_students视图;

注意:
as 关键字不能省略.
with check option 表示视图在更新时要在其权限范围内. 一般都需要加上, 保证安全性.
视图不止基于单表创建, 还可以多表, 只要select语句返回记录即可以.
视图的列数量与select后的列数量保持一致. 视图创建完成之后, 我们可以把它当作一张普通表一样使用. 包括他的增删改查.

二、更新视图

虽然说很多语法和普通表一致, 不过也有不同的.

-- 创建或更新视图
create or replace view v_students(id, name, city) as select id, name, city from students with check option;
-- 得到
+----+--------+-----------+
| id | name   | city      |
+----+--------+-----------+
|  1 | 超哥   | 杭州      |
|  2 | 孟洋   | null      |
|  3 | 梦佳   | 山沟沟    |
|  4 | 爱德   | 郑州      |
|  5 | 旭神   | 阿拉罕    |
+----+--------+-----------+
-- 同样的alter可以修改吗?
alter view v_students (name, age, city) as select name, age, city from students with check option;
-- 同样是可以的
+--------+------+-----------+
| name   | age  | city      |
+--------+------+-----------+
| 超哥   |   20 | 杭州      |
| 孟洋   |   10 | null      |
| 梦佳   |   18 | 山沟沟    |
| 爱德   |   18 | 郑州      |
| 旭神   |   88 | 阿拉罕    |
+--------+------+-----------+

当「基表」字段发生改变的时候, 我们就需要更新视图来保持数据一致.
所以开销大这也是情有可原.

可以通过replace或者alter跟新视图.

通过update更新数据

-- 更新梦佳的城市
update v_students set city="杭州" where name="梦佳";
--得到
+--------+------+-----------+
| name   | age  | city      |
+--------+------+-----------+
| 超哥   |   20 | 杭州      |
| 孟洋   |   10 | null      |
| 梦佳   |   18 | 杭州      |
| 爱德   |   18 | 郑州      |
| 旭神   |   88 | 阿拉罕    |
+--------+------+-----------+

这个时候我们再去看一下基表students;

+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |   20 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 杭州      |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

明显的看到, 我们的students表也被更新了.
是不是很熟悉, 和我们的数组很像, 浅拷贝的时候也会改变到原数据.

注意:
视图本身是没有数据, 我们对其进行的DML操作都会更新到基表中.
有很多种情况我们的视图不能做DML操作. 包括select语句含有distinct、group by、union、分组函数等.

为什么有些视图不能做DML操作

可以明显的看到, 不能DML操作的select语句中已经不是单一的表了, 这个时候我们去更新, 数据库就不知道更新哪一个数据了.

-- 更新视图
create or replace view v_students as select name,(select grade from grades where id=1) as v_grade from students with check option;
-- 得到
+--------+-----------+
| name   | v_grade   |
+--------+-----------+
| 超哥   | 一年级    |
| 孟洋   | 一年级    |
| 梦佳   | 一年级    |
| 爱德   | 一年级    |
| 旭神   | 一年级    |
+--------+-----------+
-- 更新数据
update v_students set v_grade="二年级" where name="超哥";
-- 报错, 不能更新数据
ERROR 1348 (HY000): Column 'v_grade' is not updatable

其他的不能使用DML操作的可能情况这里就不列举了.

三、视图嵌套

那我们可以基于视图继续创建视图吗?
答案是可以的.

create view v_stu_2 as select name from v_students with check option;
-- 得到
+--------+
| name   |
+--------+
| 超哥   |
| 孟洋   |
| 梦佳   |
| 爱德   |
| 旭神   |
+--------+

四、视图权限控制

我们在定义视图的时候, 可以定义视图的创建者, 以及对权限的控制.

➠ definer选项

指定视图的创建者, 没有则默认

create or replace definer="_xdq" view v_students(id, name, city) as select id, name, city from students with check option;
-- 查看视图信息
show create view v_students \G;
-- 得到
View: v_students
Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`_xdq`@`%` SQL SECURITY DEFINER VIEW `v_students` (`id`,`name`,`city`) AS select `students`.`id` AS `id`,`students`.`name` AS `name`,`students`.`city` AS `city` from `students` WITH CASCADED CHECK OPTION
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
1 row in set, 1 warning (0.00 sec)

可以看到我们的创建者_xdq

➠ 权限控制

-- 更新用户信息
update v_students set city="山沟沟" where name="梦佳";
-- 这样就报错了
ERROR 1449 (HY000): The user specified as a definer ('_xdq'@'%') does not exist

上面我们指定了视图的创建者之后, 我们的主机身份就没权限修改数据了.

这时候我们就用到另一个关键字了.

-- 使用sql security invoker加权限
create or replace definer="_xdq" sql security invoker view v_students(id, name, city) as select id, name, city from students with check option;
-- 这个时候我们再去修改数据
update v_students set city="山沟沟" where name="梦佳";
-- 得到
+----+--------+-----------+
| id | name   | city      |
+----+--------+-----------+
|  1 | 超哥   | 杭州      |
|  2 | 孟洋   | null      |
|  3 | 梦佳   | 山沟沟    |
|  4 | 爱德   | 郑州      |
|  5 | 旭神   | 阿拉罕    |
+----+--------+-----------+

注意:
sql security invoker 表示操作该视图的用户需要对基表有操作权限.
sql security definer 表示创建该视图的用户需要对基表有操作权限.

到这里, 我们视图就基本讲完了, 还有很多都需要实际操作具体分析了.

第六章 存储过程

存储过程就是一段程序脚本, 使用 SQL 语言进行编程.
存储过程会在创建后会进行预编译, 然后存储在数据库中, 使用的时候可以指定调用某一个存储过程.

使用存储过程有什么好处呢?

可以将很多比较复杂的数据操作封装成一个脚本, 后端直接调用存储过程, 而不必再在代码中写繁杂的 SQL;
可以统一接口, 确保数据安全.
存储过程作为封装好的一段脚本, 具有很好的复用性. 存储过程可以指定权限操作.

存储过程使用有什么缺点呢?

首先不利于维护, 后端代码如果依托于存储过程, 就会很繁杂.
存储过程虽然是很短的脚本, 但是调试它也并非易事.
复杂的业务逻辑配合复杂的存储过程, 那下一个迭代你需求的人会变得十分困难.

一把鼻涕一把泪

以前公司使用.net 架构, 每一个项目都看起来十分臃肿, 调用存储过程更是多而复杂, 基本每个接口都有依托存储过程, 导致 sql server 数据库十分庞大, 对于当时我这个新人十分不友好, 一段时间过后, 我存储过程写的飞起, 基本能用的地方都用 存储过程去写, 写上瘾了. 毕竟在.net 中直接掉用就能拿到想要的数据.

不过我们在 MySQL 中还是少用为好.
不必给数据库太大的压力.

不管用还是不用, 我们还是需要学会的.

一、创建存储过程

我们先来看个例子, 封装一个简单的存储过程update_age, 实现对students表年龄的修改.

-- 创建存储过程
delimiter //
create procedure update_age(in p_age int, in p_name varchar(20))
begin
    update students set age=p_age where name=p_name;
end //

注意:
因为我们一直在用命令行书写, 所以先使用 delimiter //将结束标志的分号改为//

这个时候我们看下创建好的存储过程:

-- 查看
show create procedure update_age//
-- 得到下面结果, 有很多信息我直接省略了.
...
`update_age`(in p_age int, in p_name varchar(20))
begin
    update students set age=p_age where name=p_name;
end
...

注意:
即使存储过程没有参数, 也必须在后面加上小括号().
不要把参数名写的和列名一样, 不然只会被当作列名处理.

如何调用存储过程?

使用 call 关键字调用.

-- 将超哥的年龄设置为111
call update_age(111,"超哥")//
-- 查看数据表
select * from students;
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  111 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

我们可以看到超哥的 age 已经变成 111 了.

而我们创建的存储过程是可以多次使用的, 可以随便设置任何人的年龄.
而不用每次都去写update方法了.

在创建存储过程时需要注意什么?

使用create procedure关键字创建, 可以传入参数, 同样也可以具有输出.
存储过程以begin end开始结束, 可以嵌套, 相当于我们代码中的大括号{ }. 存储过程中可以使用很多的 sql 语法, 包括 DML、DDL、以及 if、case、while 等流程控制语句.

二、存储过程中的参数

存储过程支持参数, 我们可以把每一段存储过程看成是一个代码块.

输入参数

我们上面例子中的in就是入参. 这个参数是局部变量, 和其他语言中一致.

输出参数

使用out关键字定义输出参数, 输出参数也一样放在存储过程中的括号里.

-- 创建存储过程, 输出p2, p1的基础上+1
create procedure out_test (in p1 int, out p2 int)
begin
    -- 先打印p2
    select p2;
    set p2 = p1 + 1;
    select p2;
end //

定义好了存储过程, 我们下面来调用他, 先声明两个变量p_inp_out作为入参和出参.

-- 声明入参p_in
set @p_in=2//
-- 声明出参p_out
set @p_out=1//
-- 调用存储过程
call out_test(@p_in,@p_out)//
-- 得到
+------+
| p2   |
+------+
| NULL |
+------+
1 row in set (0.00 sec)

+------+
| p2   |
+------+
|    3 |
+------+
1 row in set (0.00 sec)
-- 打印p_out
select @p_out//
-- 得到
+--------+
| @p_out |
+--------+
|      3 |
+--------+

我们可以看到, 我们声明的变量p_out被改变了.

注意:
我们传入 out 参数的时候, 这个 out 参数是对调用者输出参数, 所以我们 select 打印的时候是 null.

输入输出参数

关键字inout, inout参数具备inout的特性, 可以输入和输出. 会改变外界变量, 这里就不演示了.

三、使用 sql 编程

我们的存储过程就是一个代码块, 使用 sql 语言进行编程, 编程思维类其他语言.

定义一个变量

使用declare声明变量, 同时声明数据类型.

-- 声明变量
declare a int default 0;

给变量赋值

使用set关键字, 在上面的例子中我们就使用了.

条件语句

条件语句有if then elsecase;
和我们其他语言中的if elseswitch case类似.

-- if then else
declare a int default 0;
if a=0 then set a=2 end if;
if a=0 then set a=2 else set a=3 end if;

-- case
case a
when 0 then set a = 1
when 1 then set a = 2
when 2 then set a = 3
end case;

注意:
每条语句之后一定要使用对应的 end 语句结束.

循环语句

使用while do ... end while来进行语句循环.
使用repeat ... until end repeat来进行循环.
使用loop ... until end loop来进行循环.

-- 使用while do
while a = 0 do
set a = a + 1;
end while;

-- 使用repeat
repeat
set a = 1;
until a =0
end repeat;

-- 使用loop
loop_lable: loop
set a = a + 1;
if a > 2 then
leave loop_label
end loop;

注意:
在 sql 中没有 set a++ 这样的自增.
while 在语句执行前进行条件检查.
repeat 是在执行操作之后执行, 是不是很熟悉, 与我们的 while 和 do while 很相似.
loop 需要配合使用 leave 来离开循环, 它是没有循环条件的.

我们简单来使用一下上面的知识点

我们来做个简单的分页, 查询 students 表中年龄小于 80 的人并统计人数.

-- 创建存储过程
-- pageIndex 第几页
-- pageSize 每页几条
-- totalCount 输出 总条数
-- pageNum 总页数
create procedure sc_age(
    in params varchar(100),
    in _table text,
    in _where varchar(100),
    in _order varchar(100),
    in pageIndex int,
    in pageSize int,
    out totalCount int,
    out pageNum int
)
begin
    -- 设置变量开始行
    set @startRow = pageSize * (pageIndex - 1);
    set @pageSize = pageSize;
    set @rowIndex = 0;
    -- concat是字符串链接函数
    set @strSql = concat(
        'select sql_calc_found_rows ',
        params,
        ' from ',
        _table,
        concat(' where ', _where),
        concat(' order by ', _order),
        ' limit ',
        @startRow,
        ',',
        @pageSize
    );
    -- 预处理
    prepare strsql from @strSql;
    -- 执行预处理语句
    execute strsql;
    -- 执行完记得删除
    deallocate prepare strsql;

    set totalCount = found_rows();

    --  判断总的页数
    -- div 整除, 取整 \
    -- % 取余
    if (totalCount <= @pageSize) then
        set pageNum = 1;
    else if (totalCount % @pageSize > 0) then
        set pageNum = totalCount div @pageSize + 1;
    else
        set pageNum = totalCount div @pageSize;
    end if;
    end if;
end//

-- 调用存储过程
set @totalCoun=0//
set @pageCount=0//
call sc_age('name, age, sex, city', 'students', 'age < 80','id asc', 1,2,@totalCount,@pageCount)//
-- 可以得到
+--------+------+------+-----------+
| name   | age  | sex  | city      |
+--------+------+------+-----------+
| 孟洋   |   10 || null      |
| 梦佳   |   18 || 山沟沟    |
+--------+------+------+-----------+
-- 总的记录数
select @totalCount
-- 得到
+-------------+
| @totalCount |
+-------------+
|           3 |
+-------------+
--  总的页数
select @pageCount//
-- 得到
+------------+
| @pageCount |
+------------+
|          2 |
+------------+

注意:
上面的例子中我们使用了预处理命令, 因为我们的存储过程封装中, select语句是没有写死的,这种情况下我们就需要使用预处理语句了.
预处理语句会将sql中的值用占位符代替, 可以理解为js中的模版字符串.
存储过程的编写中, 一定要注意语法, 不要遗漏.

存储过程在实际开发中越来越少用了, 基本都是在代码中处理, 为了减轻数据库压力, 所以我们可以学, 但是尽量少用.

第七章 MySql中的事务

事务」是数据库中最基本的工作单元, 保证了一个业务的完整性, 它是一个操作序列, 这些操作要么执行, 要么不执行.

一、如何控制事务

我们可以设置是否自动提交来控制事务.
没有提交的事务具有「回滚」的功能.

我们先来查看一下MySQL中的自动提交状态.

select @@autocommit;
-- 得到
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+

显而易见, 自动提交功能默认开启.
当我们去执行一个sql语句的时候, 效果会被立即执行, 且不能回滚.
继续我们上面的例子, 使用students表.

select * from students;
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  111 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

-- 接着我们修改一条数据
update students set age=100 where name="超哥";
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  100 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+
-- 接着我们来回滚一下
rollback;
-- 结果没有改变

使用rollback来回滚更新语句, 可以看到, 并没有回滚成功.

接下来, 我们来将autocommit的状态设置为0;

set autocommit=0;
-- 得到
+--------------+
| @@autocommit |
+--------------+
|            0 |
+--------------+

然后我们接着来执行上面的操作:

update students set age=200 where name="超哥";
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  200 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+
-- 接着回滚
rollback;
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  100 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

很显然, 我们的数据表记录回到了更新语句之前.

那我们可以手动提交事务吗?

答案是肯定的! 可以使用commit语句来手动提交.

update students set age=100 where name="超哥";
commit;
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  100 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+
-- 回滚
rollback;
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  100 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

数据记录并没有回滚到我们之前的状态.

除了设置autocommit=0的方法, 还有其他方法可以手动开启事务:

  • begin;
  • start transaction;

先把之前设置的autocommit改为1.

begin;
update students set age=200 where name="超哥";
rollback;
-- 得到
+----+--------+------+-----------+------+----------+
| id | name   | sex  | city      | age  | grade_id |
+----+--------+------+-----------+------+----------+
|  1 | 超哥   || 杭州      |  100 |        2 |
|  2 | 孟洋   || null      |   10 |        1 |
|  3 | 梦佳   || 山沟沟    |   18 |        3 |
|  4 | 爱德   || 郑州      |   18 |        2 |
|  5 | 旭神   || 阿拉罕    |   88 |        4 |
+----+--------+------+-----------+------+----------+

同样的, start transaction也是一样的使用方法.

注意:
事务给我们提供一个回滚的方法rollback;
可以使用autocommit=0 、begin或者start transaction来手动开启事务;
事务开启之后, 一旦commit了, 就不可以回滚了.

二、事务的四大特征(ACID)

事务具有四大特性:

  • 原子性 A
  • 一致性 C
  • 隔离性 I
  • 持久性 D

原子性

事务是最小的单元, 不可以再分割. 如果一个操作失败了,需要将这一个事务中的所有操作恢复到事务执行之前.

一致性

同一事务中sql语句必须保证同时成功或者同时失败.

隔离性

事务具有几个隔离级别:

  • 1、read uncommitted; 读未提交
  • 2、read committed; 读已提交
  • 3、repeatable read; 可重复读 (MySQL默认隔离级别)
  • 4、serializable; 串行化

1、read uncommitted✅

如果有事务a和事务b, a事务对数据进行操作, 事务a没有被提交, 但是b可以看见a操作的结果.
举个例子🌰:

首先创建一张表user.

create table if not exists user(     
    id int,     
    name varchar(20),
    money int(200) 
);
-- 插入一条数据
insert into user values(1,"超哥",100);
insert into user values(2,"梦佳",2000);
-- 得到
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   100 |
|    2 | 梦佳   |  2000 |
+------+--------+-------+

如何查看数据库的隔离级别? 使用select查看@@global.transaction_isolation.

select @@global.transaction_isolation;
-- 得到
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

接着我们来修改隔离级别为read uncommitted;

set global transaction isolation level read uncommitted;
-- select @@global.transaction_isolation得到
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED               |
+--------------------------------+

很显然, 隔离级别被修改成READ-UNCOMMITTED了.

在read uncommitted隔离级别下会出现什么问题呢?

接着继续我们的例子:

梦佳钱💰比较多, 就想给超哥转200块钱.

begin;
update user set money=money-200 where name="梦佳";
update user set money=money+200 where name="超哥";
-- select
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   300 |
|    2 | 梦佳   |  1800 |
+------+--------+-------+
-- 可以看到转账成功

我们打开两个终端, 表示「超哥」和「梦佳」两个用户.
梦佳给超哥转了200快, 梦佳查看转账是否成功:

select * from user;
-- 得到
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   300 |
|    2 | 梦佳   |  1800 |
+------+--------+-------+

梦佳看到转账成功了. 然后超哥也要查看转账是否成功了.

select * from user;
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   300 |
|    2 | 梦佳   |  1800 |
+------+--------+-------+

超哥看到钱也到账了. 这个时候, 梦佳家比较坏了, 她回滚了一下;

rollback;

然后超哥去消费的时候, 就会发现钱不够了.

-- 没有转账成功
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   100 |
|    2 | 梦佳   |  2000 |
+------+--------+-------+

如果两个用户都在进行数据操作, 如果事务a开启之后, 它的数据可以被其他数据读取到.但是没有提交, 可以被回滚.回滚之后的数据也会被读到.
这种情况就叫做「脏读」.
「脏读」就是一个事务读到了另外一个事务没有提交的事务.

注意
实际操作中不允许脏读出现, 记得commit.

2、read committed✅

首先, 修改隔离级别为read committed.

set global transaction isolation level read committed;
-- 得到
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED                 |
+--------------------------------+

在read committed隔离级别下会出现什么问题呢?

接着我们上面的例子. 用户a计算user表中的平均值.

select avg(money) from user;
-- 得到
+------------+
| avg(money) |
+------------+
|  1050.0000 |
+------------+

然后, 用户b往user表中插入一条数据.

insert into user values(1,"孟洋",1000);
-- 得到
+------+--------+-------+
| id   | name   | money |
+------+--------+-------+
|    1 | 超哥   |   100 |
|    2 | 梦佳   |  2000 |
|    1 | 孟洋   |  1000 |
+------+--------+-------+

这个时候, 用户a重新计算了下平均值.

select avg(money) from user;
-- 得到
+------------+
| avg(money) |
+------------+
|  1033.3333 |
+------------+

显而易见, user表中的平均值被改变了.

读取同一个表, 发现前后数据不一致.
这种情况就叫做「不可重复读」.

3、repeatable read✅

首先修改隔离级别repeatable read.

set global transaction isolation level repeatable read;
-- 得到
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ                |
+--------------------------------+

在repeatable read隔离级别下会出现什么问题呢?

接着上面的例子, 我们来看下会出现什么情况.

首先, 用户a往user表中插入一条数据.

begin;
insert into user values(4,"爱德",10);
-- 得到
+----+--------+-------+
| id | name   | money |
+----+--------+-------+
|  1 | 超哥   |   100 |
|  2 | 梦佳   |  2000 |
|  3 | 孟洋   |  1000 |
|  4 | 爱德   |    10 |
+----+--------+-------+

接着用户b查询一下user表:

-- 查询user
select * from user;
-- 得到
+----+--------+-------+
| id | name   | money |
+----+--------+-------+
|  1 | 超哥   |   100 |
|  2 | 梦佳   |  2000 |
|  3 | 孟洋   |  1000 |
+----+--------+-------+

可以看到, 并没有查询到准确的数据.
那我们用户a去进行事务提交:

-- 提交
commit;
-- 用户a查询得到
select * from user;
+----+--------+-------+
| id | name   | money |
+----+--------+-------+
|  1 | 超哥   |   100 |
|  2 | 梦佳   |  2000 |
|  3 | 孟洋   |  1000 |
|  4 | 爱德   |    10 |

这时候用户b接着去查询:

-- 查询user
select * from user;
-- 得到
+----+--------+-------+
| id | name   | money |
+----+--------+-------+
|  1 | 超哥   |   100 |
|  2 | 梦佳   |  2000 |
|  3 | 孟洋   |  1000 |
+----+--------+-------+

还是没有查询到id=4的数据;
但是我们这时候用户b往user表中插入数据:

-- 插入一样的数据
insert into user values(4,"爱德",10);
-- 得到
ERROR 1062 (23000): Duplicate entry '4' for key 'user.PRIMARY'

很显然, 数据插入失败.
两个事务同时操作一张表, 事务a提交的数据, 也不能被事务b读到.
这种现象就叫做「幻读」.

注意:
关闭终端(用户b), 重新启动查询则幻读现象消失.

4、serializable✅

设置隔离级别为串行化.

set global transaction isolation level serializable;

同样的, 我们打开两个终端, 代表用户a和用户b; 接着两个用户同时开启一个事务;

-- 用户a
begin;
insert into user values(5,"狗蛋",100);
--用户b
begin 
insert into user values(6,"艾斯",1800);

可以看到先插入的用户a成功了, 但是用户b一直卡住了.
过一段时间之后, 会报一个错误, 操作超时了.

-- 操作超时
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

接下来, 用户a提交事务, 用户b再去操作插入操作, 可以操作成功了.

当一个表被一个事务操作的时候, 其他的事务里面的写操作是不能被执行的.
这种现象就是「串行化」.

注意:
如果另一个事务b还没有超时, 这时候第一个事务a被提交了, 这个事务b会被执行.
隔离级别越高, 性能越差.
read uncommitted > read committed > repeatable read > serializable
mySql默认隔离级别是repeatable read.

MySQl的事务基本就讲完了, 还有更多的用到再查了.

终章

MySQL数据库就先讲到这里了. 文章讲到了很多内容, 包括:

  • MySQL的数据类型
  • SQL编程
  • 约束
  • 视图
  • 存储过程
  • 事务
  • ... 同时为了更好的写文章, 我也列举了很多的例子.
    都是一些比较不错和常用的例子, 大佬可略过.
    不会数据库或者曾经大学学过忘掉的同学都可以学习, 简单易上手. 跟着敲一遍, 基本都可以学会了.
    MySQL的知识还有很多, 这里就不再继续了, 有兴趣的同学可以下去自行学习.
    文章体量过大, 难免有不当之处, 敬请指出, 万分感谢!
    文章太长, 我们下一篇见.

「新生代农民工」之前端工程师必会的数据库-MySQL(上)

文章粗浅, 望诸位不吝您的评论和点赞~
注: 本文系作者呕心沥血之作, 转载须声明