目录
什么是事务?
事务的四大特性?
MySql事务是如何保证ACID四大特性的?
1.Mysql是如何保证一致性的?【数据的一致性】
2Mysql是如何保证原子性的?【要么同时成功,要么同时失败】
3.Mysql是如何保证持久性的?【事务一旦提交不能进行回滚】
mysql事务的执行流程?
Spring事务传播(机制)有哪些?
在Spring中规定了7种类型的事务传播行为
Spring事务失效的场景
数据库并发问题有哪些问题?
丢失更新问题
事务隔离级别有那些?分别解决了什么问题?
读未提交(READ UNCOMMITTED)
读提交(READ COMMITTED)
可重复读(REPEATABLE READ)【默认的】
串行化(SERIALIZABLE)【乐观锁和悲观锁】:
MVCC(Multi-Version Concurrency Control)多版本并发控制
什么是丢失更新,如何解决?
第一类丢失更新【回滚丢失】:
第二类丢失更新【覆盖丢失】:
如何解决丢失更新?
什么是微服务?
什么是分布式事务?
那么如何实现分布式事务呢?又是如何进行选型的呢?
什么是2PC?
什么是TCC?
什么是CAP理论 , 哪些技术用到AP,哪些用到CP?
什么是Base理论?
事务就是操作数据库的最小执行单元,一个事务内的操作要么全部成功,要么全部失败,保证数据的一致性。
事务的四大特性(ACID):原子性Atomicity 一致性Consistency 隔离性Isolation 持久性Durability
原子性:一个事务内的操作要么全部成功,要么全部失败。
持久性:事务一旦提交【不能回滚】,将数据持久化到磁盘中,保证数据不会丢失
隔离性:【事务之间互不影响】两个事务修改同一个数据,必须按顺序执行,并且前一个事务如果未完成,那么中间状态对另一个事务不可见
一致性:【事务执行前后都必须保证数据的总和是一致的】要求任何写到数据库的数据都必须满足预先定义的规则,它基于其他三个特性实现的【【转账的前后金额是一致的,少100,那边就会多100】 】
======================ACID拓展=============================
原子性:事务的原子性是如何保证的?就是如何保证事务回滚时,能够获取到以前的数据?
主要是利用Innodb的undolog日志(回滚日志),当事务进行回滚的时候能够根据undolog日志获取到原本的数据。
持久性:持久性的关键就是事务一旦提交,不能进行回滚。通过undolog实现事务的原子性,通过redolog实现事务的持久性。
undolog是记录旧数据的备份,redolog是记录新数据的备份。
在事务提交之前,只需要将redolog日志进行持久化到磁盘当中,不需要将数据持久化到数据库中。当系统崩溃时,虽然数据没有持久化到数据库当中,但是可以通过Redolog日志获取到新数据,将所有数据恢复到最新的状态。
从数据库层面,数据库通过原子性,隔离性和持久性来保证 一致性。
所以说ACID中C(一致性【数据一致性】)是目的,原子性,隔离性,持久性三个是保证数据的一致性的手段。所以要想保证数据的一致性就需要保证事务中的原子性和隔离性和持久性。第二种,2采用排他锁,就是对当前数据进行操作时就进行加锁,其他事务不能进行操作该数据,只有等待当前线程执行完成释放锁过后才能获取锁。
使用Innodb的undo log日志(进行回滚),用来记录旧的是数据,当事务执行失败,进行回滚就可以从undolog日志中获取原本的数据。
简明知意,就是回滚日志,是实现原子性的关键,当事务回滚时能够撤销已经执行成功的sql语句,并执行日志中的sql语句恢复旧数据。
利用innodb的redo log日志。
redo log记录的是新数据的备份,在事务提交前,需要将Redo Log持久化到磁盘当中,不需要将数据持久化,当系统崩溃时,虽然数据没有持久化,系统可以根据redo Log的内容,将所有数据恢复到最新的状态
宏观上:开启事务,执行事务,提交事务,出现异常回滚事务。
实际上(举例说明):条件:事务需要将A=1修改成A=3。
执行流程:首先开启事务,将A=1存储到undolog日志用于回滚,修改A=3并将A=3存储到redolog日志(用于存储最新的数据),将redolog持久化到磁盘当中,修改成功,提交事务。
1.A.开启事务.
2.B.记录A=1到undo log.
3.C.修改A=3.
4.D.记录A=3到redo log.
5.E.记录B=2到undo log.
6.F.修改B=4.
7.G.记录B=4到redo log.
8.H.将redo log写入磁盘。
I.事务提交
Spring事务传播(Propagation)是指在一个事务方法中调用了其他的事务方法时,当前事务和被调用方法的事务如何进行交互和传播。
增删改直接使用事务的注解@Transactional
其中默认就是 Propagation propagation() default Propagation.REQUIRED; boolean readOnly() default false;
查找使用事务注解
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
Spring事务传播有7种类型,分别为:
其中,REQUIRED是默认传播行为,意味着如果当前有事务,则会加入到这个事务中,否则会新开一个事务。SUPPORTS表示如果当前有事务,就加入到这个事务中,如果没有就不处理事务。NEVER表示方法不能在事务上运行,如果当前存在事务,则抛出异常。另外,REQUIRES_NEW和NESTED都是新建一个事务来执行指定方法,不同的是,REQUIRES_NEW是在新的事务中执行,与外部事务相互独立,而NESTED则是在外部事务中启用一个保存点(Savepoint),如果嵌套事务失败,则只回滚这个保存点内的操作。
事务传播机制允许我们在一个方法调用链中控制事务边界,保证多个方法的操作都是原子性的,并且灵活控制事务的提交和回滚。
当我们在Spring中使用注解方式开启事务时,也可以指定事务传播行为。下面是一个简单的Java代码示例:
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser(User user) {
userDao.addUser(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
userDao.updateUser(user);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void deleteUser(int userId) {
userDao.deleteUser(userId);
}
}
在这个例子中,我们使用@Transactional注解来开启事务,然后在updateUser和deleteUser方法上使用@Transactional(propagation = Propagation.REQUIRED)注解,指定事务传播行为为REQUIRED。这意味着如果当前有事务,这两个方法就会加入到这个事务中,否则就会新开一个事务。
另外,addUser方法没有指定事务传播属性,因此默认使用REQUIRED事务传播行为,即如果当前有事务,就加入到这个事务中,如果没有就新建一个事务。
通过这种注解方式,我们可以很方便地指定事务传播行为,来控制事务的边界,并保证多个方法的操作都是原子性的。
下面列举几种比较常见的:
事务方法未被声明为公共方法:Spring只会拦截公共方法上的事务注解,如果你的事务方法没有声明为public,那么 Spring 就不会将其纳入事务管理范围,这会导致事务失效;
异常被try-catch捕获没有抛出: 如果事务方法在捕获了异常后没有将其重新抛出,那么事务就会被回滚,因为 Spring 完全不知道发生了异常;
声明的异常类型与抛出的异常不一致: 如果事务方法声明了一个异常类型,但是在方法内部抛出了另一种类型的异常,那么事务也会失效,因为 Spring 不会感知到这个异常;
数据库引擎不支持事务: 如果使用的是不支持事务的数据库引擎,那么事务注解就没有任何作用了,因为数据库本身就不支持事务机制;
手动提交了事务: 如果在事务方法中手动调用了commit()
方法,那么事务就不再受 Spring 管理,也就无法被回滚。
我分别使用代码举例说明这五种场景。
1.事务方法未被声明为公共方法
// 错误示例
@Transactional
private void doSomething() {
// ...
}
// 正确示例
@Transactional
public void doSomething() {
// ...
}
2.异常被try-catch块捕获
// 错误示例
@Transactional
public void doSomething() {
try {
// ...
} catch (Exception e) {
// 异常被捕获,但是没有重新抛出,导致事务失效
}
}
// 正确示例
@Transactional
public void doSomething() {
try {
// ...
} catch (Exception e) {
// 重新抛出异常
throw new RuntimeException(e);
}
}
3.异常类型没有被声明
// 错误示例
@Transactional(rollbackFor = SQLException.class)
public void doSomething() throws Exception {
// ...
// 这里抛出了RuntimeException,但是事务声明的是SQLException,导致事务失效
throw new RuntimeException();
}
// 正确示例
@Transactional(rollbackFor = RuntimeException.class)
public void doSomething() throws Exception {
// ...
throw new RuntimeException();
}
4.数据库引擎不支持事务
这种情况下,无论如何设置事务注解都无法让事务生效。
5.手动提交了事务
// 错误示例
@Transactional
public void doSomething() {
Connection conn = getConnection(); // 假设这个方法获取一个连接对象
try {
conn.setAutoCommit(false); // 关闭自动提交
// ...
conn.commit(); // 手动提交,导致事务失效
} catch (Exception e) {
conn.rollback();
} finally {
conn.close();
}
}
// 正确示例
@Transactional
public void doSomething() {
// 不需要手动提交事务
// ...
}
首先要知道为了会造成事务并发,就是当多个事务同时对同一个数据(例如数据库的一行数据)进行操作时,可能会发生数据冲突和资源争用等问题,影响系统的正确性和性能。
数据库并发可能出现那些问题呢?
1脏读:读取到的数据可能是其他事务未提交到数据库的数据。
例如,事务1先读取了一条数据,之后事务2修改了这条数据,但是事务2还没有提交,这时事务1再次读取同一条数据时,读到的是已经被修改但未提交的数据。这就会导致数据的不一致性。
2不可重复读:同一个事务中多次使用相同条件读取到的数据不一致。
例如,事务1读取了一条数据,此时事务2修改了这条数据并提交,事务1再次读取同一条数据时,读到的数据与之前读取的不一致,这种情况就称为不可重复读。
3幻读:同一个事务使用相同的条件读取到的数据条数不一致。
事务1根据某个条件查询了一条数据,此时事务2插入了一个符合条件的新数据并提交,事务1再次按照同样的查询条件再次查询时,会发现多查询出了一条数据,这就称为幻读。
但是当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,因为MySQL数据库在InnoDB存储引擎中采用了MVCC(多版本并发控制)来解决幻读问题。MVCC的实现方式是在每行数据后面添加两个隐藏字段:一个是创建时间,一个是过期时间。在读取某一行数据时,只能读取创建时间早于当前事务开始时间或过期时间为空的行,这样可以有效避免幻读问题的发生。
同时,MySQL数据库还提供了一些锁机制来避免幻读的发生,如共享锁和排他锁。在使用MySQL的InnoDB存储引擎时,可以通过使用SELECT … FOR UPDATE语句和SELECT … LOCK IN SHARE MODE语句来分别获取排他锁和共享锁,从而控制并发执行事务的锁竞争。
在MySQL 8.0版本中,还提供了一种新的隔离级别:REPEATABLE READ WITH CONSISTENT SNAPSHOT。这种隔离级别集成了MVCC和锁机制,能够有效地解决幻读问题。在这种隔离级别下,每个事务在开始时会创建一个一致性的快照,读取时只能读取该快照中的数据,就像数据是静态的一样,因此能够大大降低幻读问题的发生。
4.第一类丢失更新:也叫回滚丢失,事务A和事务B同时对同一条数据进行写操作,事务B先完成了修改,此时事务A异常终止,回滚后造成事务B的更新也丢失了
5.第二类丢失更新:也叫覆盖丢失,事务A和事务B同时对同一条数据进行写操作,事务B先完成了修改,事务A再次修改并提交,把事务B提交的数据给覆盖了【同一时间对】
脏读,幻读,不可重复读,都是发生在一个事务在写,一个事务在读的时候出现的问题。
丢失更新发生在两个事务同时都在做写操作的时候出现的
丢失更新分为:回滚丢失更新(一类)和覆盖丢失更新(二类)
首先要明白事务隔离级别是什么意思?
事务隔离级别:指数据库中的多个事务之间的隔离程度。
主要的目的就是解决数据库并发问题:脏读、幻读、不可重复读、丢失更新、
事务隔离级别包含4种:
最低的隔离级别,允许一个事务读取另一个事务还没有提交的数据。在该隔离级别下,脏读、不可重复读、幻读等并发问题都是可能发生的。
允许事务只能读取已经提交的数据,防止脏读,但是可能会导致不可重复读和幻读。
在可重复读隔离级别下,事务会创建一个一致性的快照,确保其他事务不会修改已经读取的数据(但不能保证其他事务进行insert操作)。在该隔离级别下,防止脏读和不可重复读,可能会出现幻读。
最高的隔离级别,对读、写都加上锁,确保并发执行的事务之间互相隔离,防止脏读、不可重复读和幻读,但是并发性能会大大降低。
MVCC(Multi-Version Concurrency Control)是一种并发控制技术,应用在数据库管理系统中,用于解决多个并发事务的执行问题。其底层原理包括以下几个方面:
版本号管理:MVCC 基于多版本数据模型,每次数据库中的访问都会按时间戳或版本号生成一个快照。新的事务只能查看旧版本的状态,在当前事务执行期间,任何新数据的变化都会生成一个新的版本号,从而使得数据的访问和变化能够隔离。
快照读和当前读:MVCC 支持两种访问方式:快照读和当前读。在快照读中,事务会读取之前某个时间的数据版本,而在当前读中,则会读取最新的数据版本。在当前读中,如果其它事务正在写入或者修改数据,则当前读的事务会被阻塞,直到其它事务释放锁并提交事务。
乐观锁和悲观锁:MVCC 支持两种锁机制:乐观锁和悲观锁。悲观锁基于独占锁,即事务在读取和修改数据时会将所访问的数据加锁,避免其它并发事务对相同数据的读写操作。而乐观锁则基于版本号管理,事务在读取数据时不加锁,只是在提交数据时根据版本号比较数据的变化情况,以此来处理并发冲突。
事务隔离级别:MVCC 应用在数据库事务的并发控制中,需要对事务隔离级别进行处理。不同的隔离级别会影响 MVCC 的实现策略,例如在 Repeatable Read 隔离级别下,系统会在每次事务执行时生成一个可重复读快照,隔离当前事务和其它并发事务,避免事务之间出现脏读、不可重复读等问题。
总体来说,MVCC 的底层原理主要包括版本号管理、快照读和当前读、乐观锁和悲观锁和事务隔离级别等方面。通过在数据库中使用多版本控制技术,使得事务可以在并发操作时保持数据的一致性和完整性,同时提高了数据库的并发性能。
丢失更新发生在两个事务同时都在做写操作的时候出现的。
丢失更新包括:第一类丢失更新(回滚丢失)和第二类丢失更新(覆盖丢失)
事务A和事务B两个事务同时对同一条数据进行写操作,事务B先完成了修改,此时事务A异常终止,回滚后造成事务B的更新也丢失了
以上的例子很容易解释清楚第一类丢失更新,也就是 A事务回滚时,把已经提交的B事务的更新数据覆盖了。
事务A和事务B两个事务同时对同一条数据进行写操作,事务B先完成了修改,事务A再次修改并提交,把事务B提交的数据给覆盖了。
采用乐观锁或者悲观锁进行解决。
悲观锁和乐观锁是两种思想,用于解决线程并发场景下资源竞争的问题
悲观锁,每次操作数据的时候,都会认为会出现线程并发问题(会同时对数据进行修改),一开始就会进行上锁,执行完成过后才会释放锁,线程安全,性能较低,适用于写多读少的场景
乐观锁,每次操作数据的时候,认为不会出现线程并发问题。不会使用加锁的形式而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据,如果有,则重新读取,再次尝试更新(Retry),循环上述步骤直到更新成功 。否则就执行操作。线程不安全,执行效率相对较高,适用于读多写少的场景
悲观锁一般用于并发冲突概率大,对数据安全要求高的场景。【会进行加锁,不会出现线程安全问题。乐观锁在执行更新时频繁失败,需要不断重试,反而会浪费CPU资源】
乐观锁一般用于高并发,多读少写的场景【乐观锁不会进行加锁,其他线程可以进行同时访问,提高响应效率】
乐观锁的使用场景?
ES是采用的乐观锁,因为ES的使用场景为读多写少,基本上不会出现线程并发问题。
微服务目前没有一个统一的定义,但通常而言,微服务是一种架构模式/风格。
微服务也是一个分布式系统,它将一个单体应用进行拆分成多个微服务,每个微服务独立开发、维护和部署,每个服务也都可以有自己的数据库,服务之间使用HTTP通信,互相协调完成整个系统的业务。
它的优点是服务之间解耦合,不同的服务可以有不同的编程语言,支持分布式和集群能够承载更大的服务器压力,支持敏捷开发
他的缺点是分布式事务很复杂【要保证两个业务同时成功或者同时失败】,部署麻烦,技术成本高(使用多种语言进行编程),服务间通信对性能也有一定的损耗(因为使用了http进行通信)
分布式事务指的是在分布式微服务中,一个请求需要到对多个服务器中的数据进行操作,要保证多个服务器的数据的一致性就需要用到分布式事务【要么同时成功要么同时失败】。
一个简单的分布式事务示例是一个在线电商平台,它可能有多个服务器和数据库,其中一个服务器负责处理订单,一个服务器负责处理库存,一个服务器负责处理支付等等。当一个用户从该平台上购买了一件商品时,需要对订单、库存和支付进行操作。(点击购买-生成订单-扣减库存-进行支付)。
在这个过程中,如果任何一个操作失败,整个分布式事务应该全部回滚,确保不会出现订单支付成功但库存和账户余额没有同步扣除的情况。当所有操作完成并确认无误后,整个分布式事务才会提交并完成。
常见的分布式事务实现方案,2PC/3PC,TCC,MQ最终一致性,最大努力通知等
2PC,它将整个事务流程分为两个阶段,P指的是准备阶段,C指的是提交阶段。它是一个阻塞协议,不适用于并发较高[因为需要等待参与者提交事务的状态],事务生命周期长的分布式事务。
TCC,它是基于补偿性事务的AP系统的一种实现,补偿也就是说先按照预定方案执行,如果失败了就走补偿方案。它可以自定义数据库操作的粒度(即数据统计的详细程度)。使得降低锁冲突【等待别的线程释放锁】,提高吞吐量(单位时间内成功地传送数据的数量),但是对应用的侵入性强,每个接口都需要实现try confirm cancel方法。改动成本高,实现难度大可以用在登录送积分,送优惠券等等场景
可靠消息最终一致性【rocketMQ】,系统A本地事务执行成功,并通知系统B执行,通常通过MQ实现。实现服务之间解耦,数据保持弱一致性,并且实时性要求不高的场景
最大努力通知【支付宝的支付结果通知,扣款通知】,是在不影响主业务的情况下,尽可能的保证数据的一致性,它适用于一些最终一致性敏感度低的业务,比如支付结果通知
2PC(二阶段提交)是一种常见的分布式事务实现方式,它需要一个协调者(Coordinator)和多个参与者(Participant)节点之间的协作来完成。以下是2PC的执行流程:
在这一阶段,协调者节点会向所有参与节点发送一个“CanCommit”请求,询问它们是否可以执行事务。参与节点必须检查自己的事务执行情况,确认是否可以执行该事务,如果可以执行将回复“YES”;如果不可执行,它将回复“NO”或“WAIT”(小的们回复:可以或者不可以,全部可以才干,有一个不干都不会干)。
2.指令阶段(Commit Phase)(可以干你们就干吧):
如果协调者节点接收到所有参与节点的回复都是“YES”,则它会发送一个“doCommit”请求给所有参与节点以完成事务操作。此时,所有的参与节点都会执行更新操作,并将事务操作提交到系统。如果所有的参与节点都执行成功,那么事务就成功了,否则(不能干就滚回去)协调者节点将发送一个“doAbort”请求,所有的参与节点都会回滚它们的事务操作。
3.提交阶段(Commit Phase)(全部成功才提交,有失败就不提交):
在这个阶段,协调者节点向所有参与节点发送“Commit”或者“Abort”请求,这根据第二个阶段的提交结果来决定。在接收到“Commit”请求之后,参与节点可以将它们的事务操作提交到系统中。如果它们接收到了“Abort”请求,则需要撤销之前的事务操作。
总的来说,2PC的执行流程分为三个阶段:准备、指令和提交,其中准备阶段和指令阶段都是可中止的,而提交阶段是不可撤销和不可中止的。虽然2PC是一种可靠的分布式事务实现方式,但是它本身存在一些性能瓶颈以及复杂度高的问题,会影响系统的处理效率和可扩展性。
但是也有很多缺陷
性能问题: 同步请求,需要等待所有事务参与者执行完毕,事务协调器才能通知进行全局提交,最后才会释放资源。
事务管理器单点故障:一旦事务管理器挂掉,导致参与者接收不到提交或回滚的通知。
丢失消息导致数据不一致:在提交阶段,如果事务管理器出现故障,一部分事务参与者收到了提交消息,另一部分事务参与者没有收到提交消息,造成数据不一致问题(就是通知了一部分提交失误了,然后事务管理器宕机了,造成数据不一致问题。)
TCC的执行流程如下:
1.尝试(Try)阶段:
在这个阶段,参与者(Participant)会尝试执行全部或部分事务操作,并在本地预留相应的资源(如锁定库存或扣减账户余额等),同时记录事务状态为“try”阶段。如果所有操作成功,参与者返回“yes”,否则返回“cancel”或“no”。
2.确认(Confirm)阶段:
如果所有参与者都返回了“yes”,则协调者(Coordinator)会发送“confirm”请求给所有参与者。在这个阶段,参与者会执行已经预留的资源操作,将记录事务状态修改为“confirm”阶段。如果所有操作执行成功,那么事务被提交,否则事务被回滚。
3.取消(Cancel)阶段:
如果在尝试阶段或确认阶段中任何一个参与者返回“cancel”或“no”,则协调者会发送“cancel”请求给所有参与者,这样它们就会撤销在尝试阶段中做出的所有操作,并将事务状态修改为“cancel”阶段。
相比其他的分布式事务管理方式,TCC需要更多的代码工作来实现,并且需要在业务逻辑中显式地进行事务状态的管理。但TCC更加灵活,能够更好地适应各种复杂业务场景和不同的系统架构。
CAP理论指的是,在一个分布式系统中,一致性(Consistency),可用性(Avaliability),分区容错性(Partition Tolerance),三个要素最多只能同时实现两点,不能同时兼顾。
分布式容错:指的是分布式系统某个节点或网路分区出现故障的时候,不会影响整体的使用。
可用性:一直可以正常地读写操作【但是不会保证数据的一致性】。简而言之就是客户可以一直正常访问并得到系统地正常响应,不会出现访问超时等问题。
一致性:在分布式系统中,读取数据应该是最新的数据,进行写操作后再去进行获取应该获取到最新的值,数据的一致性。
分区容错性是分布式系统的核心关键,如果一个节点或网络分区出现故障,整体就挂掉了,这就不叫作分布式系统。
满足CP,也就是满足一致性和容错性,舍弃可用性,如果系统要保证数据地强一致性(必须等待数据同步才进行响应)就可以考虑。常见的如Redis,Nacos,ZooKeeper
满足AP,也就是满足可用性和容错性,舍弃一致性,如果系统允许数据的弱一致性(最终一致性即可,获取的数据不是最新的,但最终是一致的)可以考虑。常见的如MySQL,Eureka
Base分别代表:基本可用(Basic Available),软状态(Soft State),最终一致性(Eventually Consistency)。在分布式系统中某个节点或网络分区出现故障时,系统的整体不受影响,正常使用。允许在一段时间内数据不一致,但要保证数据的最终一致性。
- Basic Availability指的是系统的基本可用性,即系统能够在故障发生时继续提供服务,但服务的质量可能会下降;
- Soft State指的是系统中的数据可以在一定时间内不同步,即不一定强一致性,但是最终会达到一致性状态;
- Eventual Consistency指的是系统在没有更新的情况下最终将达到一致性状态。