数据库,事务以及序列化相关的面试题
1 什么是事务?
事务指对数据库的一组操作,这组操作要么都实现,要么都不实现。
常规的数据库均提供了对事务的支持,即数据库本身提供了保证一组操作要么都实现,要么都不实现的功能。使用者要掌握的是操作数据库提供的事务功能的方式,及事务的相关特性。
2 事务的实现方式?
- 命令式事务
在数据库客户端下,使用SQL语句来操作事务,SQL语句分别是:
start transaction; -- 开启事务
commit; -- 提交事务
rollback; -- 回滚事务
需要特别注意的是,默认情况下,每条SQL语句独占一个事务。
- 编程式事务
通过JDBC API实现数据库事务操作,API分别是:
conn.setAutoCommit(false); // 开启事务
conn.commit(); // 提交事务
conn.rollback(); // 回滚事务
- 声明式事务
利用Spring框架提供的@Transactional
注解来实现。
@Transactional默认仅对RuntimeException及其子类异常回滚,因此我们在设计异常时,要求ServiceException继承RuntimeException,以保证事务正常回滚。
可以通过显式声明rollbackfor属性,来配置该注解对Exception回滚:@Transactional(rollbackFor=Exception.class)
@Transactional也可以添加到类上,代表该类中所有的方法都是在事务下运行的,但是不推荐这么做
3 事务的4大特性
事务具备ACID四种特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。
- 原子性(Atomicity)
事务最基本的操作单元,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency)
事务执行前后,数据库的完整性是一致的。
数据库的完整性:数据的完整性、业务的完整性。
业务的完整性例子:转账操作,事务前后,原账户和目标账户的金额总和是一致的。
a 转 1000元 给b
转账前: a余额5000,b余额 6000 a+b=11000
转账后: a余额4000,b余额 7000 a+b=11000
- 隔离性(Isolation)
并发的事务之间,应该保证彼此隔离,互不干扰。
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
- 持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。事务一旦commit,其操作结果就是持久的,后续再进行多次的rollback,也不会改变之前的结果。
4 事务的隔离级别
数据库事务的隔离级别是保证事务隔离性的解决方案。
加锁可以保证完美的隔离,同一时间内只能有一个人操作数据,但是这样一来数据库就相当于工作在单线程的状态下,同一时间只能有一个事务操作,并发的效率非常低下
而现实生活中,并不是所有的场景下都需要那么严格的事务隔离,在不同的业务场景下对隔离性的要求是不同。
所以数据库的设计者设计了不同的隔离级别,让使用者可以在隔离能力和性能间做一个权衡。
事务的隔离级别有4种,由低到高分别为Read uncommitted、Read committed、Repeatable read、Serializable。
事务的隔离级别越高,事务与事务之间彼此的干扰越少,安全性越高,但是消耗的资源也越多。反之,隔离性越低,消耗资源越少,效率越高,但是安全性也越差。
因此,事务的隔离级别不是越高越好,是在当前业务安全性允许的条件下,选择最低的隔离级别。
Mysql数据库的默认隔离级别是:Repeatable read
Oracle数据库的默认隔离级别时:Read committed
脏读、不可重复读、幻象读概念说明:
脏读:一个事务使用了另一个事务未提交的数据。
不可重复读:指在一个事务内,多次读同一数据,得到的结果不同。
幻读:一个事务读取全表数据时,读取到另一个事务向表中新增、删除操作提交的结果
4种隔离级别具体说明:
- Read uncoommitted
读未提交,一个事务可以读取到另一个事务未提交的操作的结果。该级别不保证任何的隔离性。
该隔离级别可能出现脏读、不可重复读、幻读问题。
举例:
例子前的建表语句:
create table account(
id int primary key auto_increment,
name varchar(10),
money int
);
insert into account values(1,'a',1000),(2,'b',1000);
查询数据库隔离级别的SQL语句:
select @@tx_isolation;
设置数据库的隔离级别的SQL语句:
set [session/global] transaction isolation level xxxxxx;
session:修改当前会话的隔离级别,不影响其他会话。当前客户端关闭,则修改失效。
global:修改数据库的默认隔离级别,对所有后续新建的会话生效,对之前已经存在的会话无效。
案例演示:
a:
set session transaction isolation level read uncommitted;
---------------
a: 1000
b: 1000
---------------
b:
start transaction;
update account set money = money - 100 where name = 'b';
update account set money = money + 100 where name = 'a';
a:
start transaction;
select * from account;
---------------
a: 1100
b: 900
---------------
commit;
b:
rollback;
a:
start transaction;
select * from account;
---------------
a: 1000
b: 1000
---------------
commit;
- Read committed
读已提交,一个事务仅能读取到另一个事务已提交的数据。
可以保证部分隔离性,可以防止脏读问题,但是具有不可重复读和幻读问题。
不可重复读不一定是问题,仅在一些特殊的场景下,会成为问题。
例子前的建表语句:
create table account2(
name varchar(10),
money1 int, -- 现金
money2 int, -- 活期存款
money3 int -- 定期存款
);
insert into account2 values('a',1000,1000,1000);
a:
set session transaction isolation level read committed;
-------------------------
a 1000 1000 1000
-------------------------
b:
start transaction;
select money1 from account where name = 'a'; -- 活期存款1000
select money2 from account where name = 'a'; -- 定期存款1000
select money3 from account where name = 'a'; -- 固定存款1000
-------------
a:
start transaction;
update account2 set money1 = money1 - 1000 where name = 'a';
commit;
-------------
b:
select money1 + money2 + money3 from account where name = 'a'; -- 总资产2000
- Repeatable read
可重复读,该级别下,一个事务无法读取另一个后开启的事务已提交的操作结果。
可以防止脏读和不可重复读问题,但是会出现幻读问题。
举例:
set session transaction isolation level Repeatable read;
-------------------------
a 1000
b 1000
-------------------------
a:
start transaction;
select count(*) from account; -- 2人
select sum(money) from account; -- 2000元
--------
d:
start transaction;
insert into account values(d 4000);
commit;
-------------------------
a 1000
b 1000
d 4000
-------------------------
--------
select avg(money) from account; -- 2000元
commit;
- Serializable
保证完全隔离,可以防止脏读,不可重复读,幻读问题,本质上是靠锁来实现的
5 事务的传播机制
在使用Spring的声明式事务,即@Transactional
注解时,可以使用@Transactional(propagation = Propagation.REQUIRED)
来配置事务的传播行为。
spring在TransactionDefinition
接口中定义了七个事务传播行为,也可以通过Propagation
枚举类来调用:
REQUIRED(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。
SUPPORTS:支持使用当前事务,如果当前事务不存在,则不使用事务。
MANDATORY:中文翻译为强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。
REQUIRES_NEW:创建一个新事务,如果当前事务存在,把当前事务挂起。
NOT_SUPPORTED:无事务执行,如果当前事务存在,把当前事务挂起。
NEVER:无事务执行,如果当前有事务则抛出Exception。
NESTED:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。
6 数据库表中的外键
foreign key约束: A表中的一个字段关联了B表中的主键字段
foreign key的基本作用:A表中该字段的值,必须是B表中主键字段中已经存在的值
数据库的外键约束可以自动进行关联数据的校验,有利于保证数据库中数据的一致性。但是,外键约束会较大的影响并发查询时的效率,同时不利于项目的可扩展性。
基于上述原因,在大多数互联网企业中,要求数据库中不能显式声明外键约束,而采用逻辑外键的方式来实现。
逻辑外键:使用编程的方式,在程序中实现外键形式的校验。
具体例子:在向角色菜单表中插入关联数据之前,先验证插入的菜单id在菜单表中都有对应的记录。
序列化小面试题
所有的实体类都需要添加序列化接口吗?
-
为什么要实现序列化接口?标识该类可以被序列化,并为该类添加一个序列化ID
-
序列化是用来做什么的?将Java对象中的数据以固定的格式写入到一个文件中,可以将该文件发送给需要使用对象数据的程序,再使用反序列化,根据该文件,在内存中创建对应的对象。
-
Web项目中什么时候需要序列化?
a: 业务层可能被做成第三方接口,供其他程序调用,包括微服务场景,这些场景下,彼此传递的实体类必须实现序列化接口 b: HttpSession中保存的对象,需要实现序列化接口,因为Session执行机制中,包含自动将数据序列化到本地文件保存的机制。
综上,很多企业要求所有的实体类都实现序列化接口,增加项目的可扩展性。