【CAP】数据库的竞态问题

有并发就有竞态,就有锁的问题。

最基本的是数据库的竞态问题。

一、脏读、不可重复读、幻读

1、脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

例如:

张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。

与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。

随后,事务A发生异常,而回滚了事务。张三的工资又回滚为5000。

最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。

2、不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

例如:

在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。

与此同时,事务B把张三的工资改为8000,并提交了事务。

随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。

3、幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

例如:

目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。

此时,事务B插入一条工资也为5000的记录。

这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。

4、提醒

不可重复读的重点是修改:

同样的条件,你读取过的数据,再次读取出来发现值不一样了

幻读的重点在于新增或者删除:

同样的条件,第 1 次和第 2 次读出来的记录数不一样

二、独占锁、共享锁、更新锁,乐观锁、悲观锁

1、锁的两种分类方式

###(1)从数据库系统的角度来看,锁分为以下三种类型:

独占锁(Exclusive Lock)

独占锁锁定的资源只允许进行锁定操作的程序使用,其它任何对它的操作均不会被接受。执行数据更新命令,即INSERT、 UPDATE 或DELETE 命令时,数据库会自动使用独占锁。但当对象上有其它锁存在时,无法对其加独占锁。独占锁一直到事务结束才能被释放。

共享锁(Shared Lock)

共享锁锁定的资源可以被其它用户读取,但其它用户不能修改它。在SELECT 命令执行时,数据库通常会对对象进行共享锁锁定。通常加共享锁的数据页被读取完毕后,共享锁就会立即被释放。

更新锁(Update Lock)

更新锁是为了防止死锁而设立的。当数据库准备更新数据时,它首先对数据对象作更新锁锁定,这样数据将不能被修改,但可以读取。等到数据库确定要进行更新数据操作时,它会自动将更新锁换为独占锁。但当对象上有其它锁存在时,无法对其作更新锁锁定。

###(2)从程序员的角度看,锁分为以下两种类型:

悲观锁(Pessimistic Lock)

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

乐观锁(Optimistic Lock)

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

三、事务五种隔离级别

在竞态数据对不同事务的可见性。

Isolation 属性一共支持五种事务设置,具体介绍如下:

(1)DEFAULT

使用数据库设置的隔离级别(默认),由DBA 默认的设置来决定隔离级别。

(2)READ_UNCOMMITTED

这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。

会出现脏读、不可重复读、幻读 (隔离级别最低,并发性能高)。

(3)READ_COMMITTED

保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

可以避免脏读,但会出现不可重复读、幻读问题(锁定正在读取的行)。

(4)REPEATABLE_READ

可以防止脏读、不可重复读,但会出幻读(锁定所读取的所有行)。

(5)SERIALIZABLE

这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。

保证所有的情况不会发生(锁表)。

MYSQL悲观锁的使用

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性。

1
2
3
4
5
6
7
8
9
10
11
12
set autocommit=0;  
//设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:
//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;

上面的第一步我们执行了一次查询操作:select status from t_goods where id=1 for update;与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_goods表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。