什么是锁?
锁的存在是为了数据的一致性,我们都知道mysql在修改数据层面是支持并发修改的,那么在多个线程同时修改一个数据时产生的线程安全问题;什么是线程安全呢?大家想象这样的场景,一个数据,在高并发场景下,如果大家都去修改它,那这个数据不就乱套了,所以就有了锁的限制,大家排队来修改这个数据,必须上一个人改完之后,下一个人才能进行修改;
关于锁这个概念,还有另一种解释, 想象这样的场景,马桶只有一个,有10个人都想要上厕所,那么这个马桶同一时间只能被一个人使用(我从没见过2个人同时用一个马桶的场景),第一个人进去之后,就会把们反锁,只有这样,外面的人才进不来,只能在外面等着,当第一个人方便完之后解锁,然后出来,第二个人才能进去,第二个人出来第三个人才能进去,以此类推,一直到第10个人上完;这就是锁的概念。
mysql有三个存储引擎,分别是
- memory
- myisam
- innodb
每个存储引擎都有不同的锁方式,分别为表锁和行锁
表锁
表锁指的是将整个表锁住,锁住后,任何人都不能往表中添加、修改或者删除数据;这是一种独占思想,也就是当表被某一个线程独占后,在独占的这段时间里,这个线程可以对表做任何操作,而别的线程只能眼巴巴地看着;就跟承包鱼塘一样,比如我承包了一块鱼塘一年时间,那么在这一年时间里面我可以对鱼塘做任何事情,养鱼养虾养王八,别人都管不着。
行锁
行锁比较容易理解,就是之锁一行数据,还拿上面池塘的例子来说,如果把池塘比喻成表,那么池塘里的鱼就是一行行的数据,池塘里会有成千上万条鱼,而你表里面的数据也是成千上万条的的,锁住一行记录,就相当于是抓住某一条鱼;拔掉它的鳞片或者是把这条鱼揍一顿就是修改这条鱼的数据,修改完后在将鱼放回池塘;这就是行锁。
memory
内存级别的表,不支持持久化存储,断电丢失数据,只支持表锁。
myisam
myisam只支持表锁,并且也不支持事务;所有的读写操作都是串行的
myisam在执行查询语句之前,会自动给涉及的所有表加读锁,执行更新操作前,会自动给涉及的表加写锁,这个过程不需要用户干预,因此用户一般不需要使用命令来显示加锁,下面的例子加锁时是为了演示效果,
表共享读锁
加读锁命令
lock table table_name read;
-- 解锁
unlock tables;
当前线程读,其他线程读(myisam)
加上读锁后,所有会话都可以读取A表数据,并且不会受影响,也不会阻塞
当前会话先读后写(myisam)
一旦给当前会话加上共享读锁后,就不能有写操作,,否则会报错,写操作包括(insert、update、delete)
当前会话程读,其他线程写(myisam)
加上共享读锁后,其他线程线程写入会进入阻塞状态,待解锁后可进行写操作;
会话1给A表加读锁,会话1在查询查询其他表(myisam)
会话1给A表加读锁后,再读其他表会报错,要先释放A表的读锁才可以读其他表;这是一个比较特别的知识点
只锁当前会话的读锁(read local)
加上下面的语句之后,当前会话可读,不可写,其他会话可写可读;
-- local表示本地
lock table table_name read local;
但是会有并发插入问题,也就是说,在当前会话开启read local 锁之后,在别的会话插入的数据,当前会话查询不到;
表独占写锁
加写锁命令:
lock table table_name write;
-- 解锁
unlock tables;
加上写锁后当前线程可以进写入操作,当前线程也可以读,但是其他线程的读写操作都会阻塞;
innodb
事务的 四个特征(ACID)
innodb支持表锁和行锁,innodb锁的对象是索引,聊到innodb的索引,肯定要牵扯到事务,事务有4个属性,称为ACID属性
- 原子性(Actimicity): 事务是原子操作,要么同时修改,要么同时回滚
- 一致性(Consistent):事务完成时必须保证数据一致性
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作时影响
独立
的环境之下 - 持久性(Durable):也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
脏读
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。
幻读
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。 如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
不可重复读
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果 只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也越大,因为事务隔离本质上就是将事务在一定程度上串行化,需要根据具体的业务需求来决定使用哪种隔离级别,
innodb的锁分为共享锁、排他锁、意向共享锁、意向排他锁、间隙锁、自增锁,间隙锁和自增锁问的比较少;
- 共享锁,可以理解为读锁
- 排他锁,可以理解为写锁;
==注意:演示下面的案例时需要先关闭自动提交的功能==
加共享锁(innodb)
select * from table where id = 1 lock in share mode;
事务1读,其他事务也可以读(innodb)
加了共享锁后,所有线程都可以读数据
事务1读,事务2写会阻塞(innodb)
当开启读锁后,当前事务可以读数据,其他事务也可以写数据,但是需要等事务1释放锁后才可以写,所以当其他事务进行写的时候会进入阻塞状态。
事务A写会报错(innodb)
当开启读锁后,当前事务只能读数据,而不能写数据,写数据时会报错。
加排它锁(innodb)
select * from table where id = 1 for update;
- 事务A获取锁,其他的的事务都会进行阻塞等待
需要注意的是,innodb是支持行锁的,行锁是通过给索引项加锁实现的,所以你的表里面必须要有索引,否则就会退化成表锁;另外,insert 插入数据的时候因为你是针对整张表插入,而不是针对某一行数据,所以insert也是表锁;
意向共享锁
事务准备给数据行加入共享锁,也就是说一个数据行在加共享锁之前必须先取得意向共享锁;
意向排他锁
事务准备给数据行加入排他锁,也就是说一个数据行在加排他锁之前必须先取得意向排他锁;
意向这个东西,你就理解为加锁之前的准备工作,就好像我吃饭之前需要拿出筷子一样
==意向锁是innodb存储引擎操作数据之前自动加的,不需要用户干预==
自增锁
下面的 auto_increment
表示该字段为自增字段,默认每次自增1
create table tablename(
id int auto_increment primary key,
name varchar(20)
)
自增锁是保证每次都自增的,比如有如下场景
insert into tablename (name) values('yexindong'); -- 第1
insert into tablename (name) values('yexindong'); -- 第2
commit; -- 提交
insert into tablename (name) values('yexindong'); -- 第3
insert into tablename (name) values('yexindong'); -- 第4
rollback; -- 回滚
insert into tablename (name) values('yexindong');-- 第5
commit; -- 提交
假如我们的自增id 是从1开始的,前两次插入的提交了,id为1和2;
虽然第3、4次的插入回滚了,
但是第五次插入的id其实是5,有人会说了,3和4不是回滚了吗?按理说第5次插入的id应该是3啊;因为自增锁为了保证自增的,3和4已经被使用过了,不管你有没有插入,还会往上自增,所以第五次插入的id是5,而不是3;
模拟innodb死锁
==注意:演示下面的案例时需要先关闭自动提交的功能==
以下4个步骤下来,就会产生死锁
1、事务A
select * from user where user = 1 for update
2、事务B
select * from user where user = 2 for update
3、事务B
select * from user where user = 1 for update
4、事务A
select * from user where user = 2 for update
以上原理是因为谁都不想释放,又想得到对方的锁,所以就会产生死锁,就好像两个人吵架了,都不肯道歉,都在等对方认错,然后关系就越来越僵,最后导致绝交;
行锁演示
比如我有这么以下user表数据
create table user(
id int(20) parmary key,
name varchar(20)
);
-- 给name加上普通索引
alter table user add index index_name(name);
-- 插入数据
insert into user (id,name) values(1,'yexindong');
insert into user (id,name) values(2,'zhangsan');
下面的例子,我查询下面2条语句时锁的其实是同一行,会有冲突
-- 查询下面2条语句时锁的其实是同一行,会有冲突
select * from user where id = 1 for update;
select * from user where name = 'yexindong' for update;
操作不同的行,不会有冲突,
-- 以下2条sql锁的是不同的行,不会冲突
select * from user where id = 1 for update;
select * from user where name = 'zhangsan' for update;
完
关于mysql的锁知识,到这里就讲完了,其实java里面的锁更多,而且锁的知识都是一通百通的,知道java 的锁就一定了解mysql的锁,那么问题又来了,你知道mysql的锁是公平锁还是非公平锁呢?