Java面试--数据库(事物/事物隔离级别/乐观锁与悲观锁)

数据库

一、关系型数据库

基于关系代数理论;

缺点:表结构不直观,实现复杂,速度慢

优点:健壮性高,社区庞大

二、事物

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。  
事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。  

事务的 ACID  
事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。

1 、原子性  
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做  
2 、一致性  
事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。  
3 、隔离性  
一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。  
4 、持续性  
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。  


数据库系统必须维护事务的以下特性 ( 简称 ACID) :  
原子性 (Atomicity)  
一致性 (Consistency)  
隔离性 (Isolation)  
持久性 (Durability)  
原子性 (Atomicity)  
事务中的所有操作要么全部执行,要么都不执行;  如果事务没有原子性的保证,那么在发生系统  
故障的情况下,数据库就有可能处于不一致状态  

三、事物的隔离级别【参考博客】

事务在操作时的理想状态:事务与事务之间不会有任何影响

MySQL数据库有四种隔离级别

上面的级别最低,下面的级别最高。“是”表示会出现这种问题,“否”表示不会出现这种问题。

MySQL事务隔离级别相关的命令

查询全局事务隔离级别
​
查询隔离级别 
-- select @@tx_isolation;
​
设置事务隔离级别,需要退出MySQL再重新登录才能看到隔离级别的变化
​
设置隔离级别 
-- set global transaction isolation level 级别字符串;  

转账的操作

查询全局事务隔离级别
​
查询隔离级别 
-- select @@tx_isolation;
​
设置事务隔离级别,需要退出MySQL再重新登录才能看到隔离级别的变化
​
设置隔离级别 
-- set global transaction isolation level 级别字符串;  

脏读的演示

将数据进行恢复:UPDATE account SET balance = 1000;
​
打开A窗口登录MySQL,设置全局的隔离级别为最低
​
mysql -uroot -proot
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
​
打开B窗口,AB窗口都开启事务
USE db;
START TRANSACTION;
​
A窗口更新2个人的账户数据,未提交
UPDATE account SET balance=balance-500 WHERE id=1;
UPDATE account SET balance=balance+500 WHERE id=2;
​
​
B窗口查询账户
SELECT * FROM account;
​
​
A窗口回滚
ROLLBACK;
​
​
B窗口查询账户,钱没了 
SELECT * FROM account;
​
脏读非常危险的,比如Jack向Rose购买商品,Jack开启事务,向Rose账号转入500块,
然后打电话给Rose说钱已经转了。
Rose一查询钱到账了,发货给Jack。
Jack收到货后回滚事务,Rose的再查看钱没了。

解决脏读的问题:将全局的隔离级别进行提升

将数据进行恢复:
UPDATE account SET balance = 1000;
​
在A窗口设置全局的隔离级别为*READ COMMITTED*
​
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
​
​
B窗口退出MySQL,B窗口再进入MySQL 
​
​
AB窗口同时开启事务 !
​
​
A更新2个人的账户,未提交
UPDATE account SET balance=balance-500 WHERE id=1;
UPDATE account SET balance=balance+500 WHERE id=2;
​
​
B窗口查询账户
​
​
A窗口commit提交事务
COMMIT;
 
B窗口查看账户 ​

结论:read committed的方式可以避免脏读的发生却有不可重复读的问题

不可重复读的演示

将数据进行恢复:
UPDATE account SET balance = 1000;
​
开启A窗口
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
​​
​
开启B窗口,在B窗口开启事务
​
START TRANSACTION;
SELECT * FROM account;
​
​
在A窗口开启事务,并更新数据
​
START TRANSACTION;
UPDATE account SET balance=balance+500 WHERE id=1;
COMMIT;
​
B窗口查询
​
SELECT * FROM account;
​
两次查询输出的结果不同,到底哪次是对的?不知道以哪次为准。 
很多人认为这种情况就对了,无须困惑,当然是后面的为准。
我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客户,
结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,
银行工作人员就不知道以哪个为准了。

解决不可重复读的问题

将全局的隔离级别进行提升为:repeatable READ 
​
将数据进行恢复:
​
UPDATE account SET balance = 1000;
​
A窗口设置隔离级别为:*REPEATABLE READ*
​
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
​
​
​
B窗口退出MySQL,B窗口再进入MySQL
​
START TRANSACTION;
SELECT * FROM account;
​
​
​
A窗口更新数据
​
START TRANSACTION;
UPDATE account SET balance=balance+500 WHERE id=1;
COMMIT;
​
​
B窗口查询
​
SELECT * FROM account;
​
​
​
结论:同一个事务中为了保证多次查询数据一致,必须使用*REPEATABLE READ*隔离级别 

练习

1. 脏读的操作:
 1) 打开命令行Jack,设置全局的隔离级别为最低:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; 
​
 2) 开启事务
 USE db;
 START TRANSACTION;
​
 3) 更新2个人的账户,未提交
 UPDATE account SET balance=balance-200 WHERE NAME='Jack';
 UPDATE account SET balance=balance+200 WHERE NAME='Rose';
​
 4) 打开另一个命令行Rose,查询账户
 USE db;
 SELECT * FROM account;
 发现钱已经到账,发货
​
 5) 命令行Jack回滚
ROLLBACK;
​
 6) 命令行Rose,查询账户,钱没了。  
​
2. 解决脏读的问题:将全局的隔离级别进行提升
 1) 打开命令行Jack,设置全局的隔离级别为read committed:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; 
​
 2) 重复上面的操作:需要重新登录。
会发现命令行Jack在没有提交和回滚之前,命令行Rose看不到账户发生任何变化。
​
 3) 命令行Jack,使用commit提交以后
命令行Rose,可以看到账户发生了变化。
​
 4) 结论:read committed的方式可以避免脏读的发生。
​
3.  出现不可重复读的问题
 1). 将数据进行恢复,并关闭窗口重新登录。
UPDATE account SET balance=500;
​
 2) 开启1个银行账户窗口
SELECT @@global.tx_isolation; -- 确保当前的事务隔离是read committed
​
 3) 在银行窗口中开启一个事务
USE db;
START TRANSACTION;
查询用户Jack的账户,输出到屏幕。
SELECT * FROM account WHERE NAME='Jack';  -- 查到是500块
​
 4) 开启1个Jack账户窗口
-- 开启一个事务:
USE db;
START TRANSACTION
更新账户,加100元
UPDATE account SET balance=balance+100 WHERE NAME='Jack';
提交事务
COMMIT;
​
 5) 银行窗口在同一个事务中再次查询Jack的账户,输出到文件中:
SELECT * FROM account WHERE NAME='Jack';  -- 查到是600块
COMMIT;  -- 事务结束
两次查询输出的结果不同,到底哪次是对的?
​
4. 解决:不可重复读的问题
 1). 将数据进行恢复
UPDATE account SET balance=500;
设置隔离级别为repeatable READ  
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; 
SELECT @@global.tx_isolation;  -- 查询隔离级别
​
 2) 重新登录
银行窗口登录
USE db;
START TRANSACTION;
SELECT * FROM account WHERE NAME='Jack';-- 查询到500
​
 3) Jack窗口登录
USE db;
START TRANSACTION;
SELECT * FROM account WHERE NAME='Jack';
UPDATE account SET balance=balance+100 WHERE NAME='Jack';
COMMIT;  -- 提交事务
SELECT * FROM account WHERE NAME='Jack'; -- 这时Jack看到的是600
​
 4) 银行窗口再查
SELECT * FROM account WHERE NAME='Jack'; -- 查询还是500,很好的体现了事务的隔离性。
COMMIT;-- 2次查询结果相同
​
 5) 银行提交当前事务
COMMIT;
再开启一个新的事务
START TRANSACTION;
SELECT * FROM account WHERE NAME='Jack';  -- 查询变成了600
COMMIT;
​
 6) 结论:为了保存多次查询数据一致,必须使用repeatable read隔离级别
​
5. 幻读的操作:
1) 开启一个银行窗口
USE db;
UPDATE account SET balance=500;  -- 还原数据
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;  -- 设置隔离级别为最高
SELECT @@global.tx_isolation;  -- 显示事务的隔离级别
​
 2)  关闭银行窗口,重新登录
START TRANSACTION;   -- 开启事务
SELECT COUNT(*) FROM account;  -- 查询一共有多少个账户,可以多次查询
​
 3) 开启另一个New窗口
USE db;
START TRANSACTION; -- 开启事务
INSERT INTO account (NAME,balance) VALUES ('New', 500);  
-- 这时会发现这个操作无法进行,除非银行窗口中的事务结束
​
 4) 在银行窗口中提交事务
COMMIT;
则New窗口中insert语句运行
​
 5)New窗口中要commit或rollback,完成当前事务。
银行窗口就能看到最新的数据。
否则就会出现银行窗口中2次查询总数不同的情况。

四、乐观锁与悲观锁原理【参考博客】

乐观锁

 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

 version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

 CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

悲观锁

 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

综合例题

A:正确,缓存命中率提高了,程序性能也会提高;

B:错误,I/O 密集型操作:所有的线程都在等待 I/O 的操作,大部分时间都在等待中,使用多线程帮助不大;

C:正确,利用数据库连接池可以节约每次对数据库建立连接的时间

D:正确:递归使用栈会带来消耗,希望使用迭代取代递归

E:正确:远程调用需要发送数据、等待数据这一来回过程,这一过程耗时较长;合并多个远程调用批量发送是常用的软件调优方式;

F:正确:共享冗余数据可以提高访问效率

猜你喜欢

转载自blog.csdn.net/jianghao233/article/details/82765301