目录
前言
锁
锁按照粒度分,可以分为两种:
锁按照类型来分,也可分为两类:
悲观锁和乐观锁
版本号机制
CAS
事务
什么是事务?
事务的特性
数据库中的事务
查询命令
关闭命令
开启命令
事务的管理
数据库中的死锁
数据库中的视图
什么是视图?
操作视图:
视图的注意事项
事务隔离级别
读未提交
读已提交
可重复读
可串行化
用一张表来表示他们之间的关系:
隔离等级的优先级
数据库使用的隔离级别
MVCC
隐藏字段作用
ReadView
结语
前言
常用的数据库有哪些呢?以博主的认知为例,见过最多的就是以下这三种了:
oracle,sql server和mysql
虽然同为数据库,数据结构也是存在着一些细微的差别的,这个我们后面会简单提到,毕竟都是按照sql的统一规范设计的数据库,才有了相同的增删改查功能,所以他们的差别也大不到哪去,这里先提一嘴,这三者的隔离级别不同。
其实本来也想写写sql语句中的一些坑和注意事项的,但想了想,放在数据结构里略微不合适,还是后期专门开一篇讲讲sql语句中踩到的坑和注意事项吧。
锁
锁按照粒度分,可以分为两种:
表锁行锁从名字也不难推测出它们所处的位置。顾名思义,表锁是加在整张表上的,其他的事务将无法在当前事务执行期间访问整张表,行锁是加在单独的某一行上的,其他事务无法在当前事务执行时访问这一行。这也是我们平时所说的线程安全的一部分,只不过是放在数据库上面的。
锁按照类型来分,也可分为两类:
共享锁
排他锁共享锁也叫share锁/s锁,既可以给表加,也可以给行加,因为其共享特性,所以加上了共享锁的数据,允许其他事务同时访问,但是绝对不允许其他事务给他再加排他锁,却依然可以允许其他事务加共享锁。多用于读操作。
排他锁有些地方也叫独占锁/x锁,被加上排他锁的数据,既不允许其他事务再加共享锁,也不允许其他事务再加排他锁,同时,在当前事务执行结束前,其他事务无法访问。多用于写操作。
数据库中,我们经常使用的读写sql语句为:
select....
insert...
delete...
update...
其中,增删改操作默认给操作的行数据加排它锁,select操作默认不加任何锁。
如果需要在查询时加锁,怎么做呢?
查询加共享锁
select...... 所有条件之后 + lock in share mode
查询加排它锁
select...... 所有条件之后 + for update
悲观锁和乐观锁
悲观锁和乐观锁不同于行锁和表锁,他们只是两种思想。
悲观锁:当多线程或者事务并发执行的时候,事务会悲观的认为,在自己访问数据期间,其他事务一定会同时访问,为了安全着想,事务再访问数据时,立即给这条数据加锁以保证数据是线程安全的。这很像是一个对自身保护过度的人,所以由此可见,悲观锁的效率一定是偏低的。排他锁符合这种特性,所以排他锁都是悲观锁。
乐观锁:和悲观锁相反,数据访问期间,事务很乐观,认为在自己访问期间不会有其他的事务访问,也不会有线程安全问题,所以就不加锁。但在实际中这是不存在的,一定会存在同时访问的情况,这时候,乐观锁会通过其他的机制来保证线程的安全,分别是版本号机制或CAS(compare and swap 比较并交换)。
假设我有一张user表:
id | name | age |
1 | 张三 | 20 |
2 | 李四 | 25 |
两个线程
线程A:查询张三,并修改其年龄为23 1.查询张三数据 2.执行修改 3.提交事务 |
线程B:查询张三,并修改其年龄为24 1.查询张三数据 2.执行修改 3.提交事务 |
若是两个线程同时执行,那么A预期的年龄为23,B预期的年龄为24,不管谁先执行,最后得到的数据,只能是符合其中一个的预期,这样就有问题了。
我们用悲观锁的思想来解决,给行数据加锁,就会变成其中一个先执行,执行完之后另一个线程再执行,这当然可以。但我们想要用乐观锁来做,该怎么做呢?需要使用版本号机制。
版本号机制
依然是这张表,只是多了一个版本号,初始值我们给0.
id | name | age | version |
1 | 张三 | 20 | 0 |
2 | 李四 | 25 | 0 |
两条线程一起执行
线程A:查询张三,并修改其年龄为23 1.查询张三数据,version=0 2.执行修改,age=23,version=0 3.提交事务,需要对比版本号是否等于查询时的版本号0,相等,执行成功,version+1=1. |
线程B:查询张三,并修改其年龄为24 1.查询张三数据,version=0 2.执行修改,age=24,version=0 3.提交事务,需要对比版本号等于产巡视的版本号0,此时版本号为1,不等,执行失败。 |
版本号机制在提交事务的时候会对查询时的版本号进行对比,对比相等,提交,执行成功,对比 不想等,执行失败,事务回滚。
失败后,因为乐观锁中有一个自旋机制,在失败后会重新发起修改,此时线程A已经执行完毕,线程B中心执行,此时version=1,查询到version=1,提交时,只要没有其他线程参与,version仍为1,提交成功,此时version=2。如果有其他的线程参与导致此次修改再次失败,自旋不会无限自旋,它有一个设置时间,可自定义。
CAS
CAS其实在理解了版本号机制之后就很好理解了。
还是这张user表:
id | name | age |
1 | 张三 | 20 |
2 | 李四 | 25 |
还是这两个线程:
线程A:查询张三,并修改其年龄为23 1.查询张三数据 2.执行修改 3.提交事务 |
线程B:查询张三,并修改其年龄为24 1.查询张三数据 2.执行修改 3.提交事务 |
俩线程执行完之后,必然会有其中一个的值被覆盖,这就产生了线程安全问题。在版本号机制中,我们比较的是version,但是在CAS中,我们比较的是值本身,如果修改的时候值和查询时的值不一样,那么进行回滚,自旋机制启动,这时候就和version机制是一样的了。
这样一看,版本号机制和CAS是不是很相似呢?只是比较的对象不一样。
现在,你理解这两种机制了么?
在这里给大家留一个问题,悲观锁和乐观锁哪一个用的多?大家可以评论区讨论下并说说具体原因?
事务
什么是事务?
事务是数据库中执行操作的最小执行单元,不可再分,要么全都执行成功,要么全都执行失败。
事务的特性
原子性
事务是数据库的逻辑工作单位,原子性是指事务包含的所有操作要么全部成功,要么全部失败。
一致性
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。一致性与原子性是密切相关的。
隔离性
一个事务的执行不能被其他事务干扰
持久性
一个事务一旦提交,它对数据库中数据的改变就应该是永久性的
在前面,我们已经接触到了一些特性,比如原子性,其他的特性也都是相关的联的,不可能单个存在。