一、事务的作用:
1)并发情况下如何保持数据一致性的问题?通过事务可以解决。
2)如果对数据库的操作失败了,可以恢复到正常状态。
3)通过设置隔离级别,防止彼此的操作相互干扰。
二、核心理论:
1、事务所具备的特征:ACID
1)同时成功同时失败其实说的是事务的原子性,不可分割。
2)事务执行前后的逻辑一致性;账户加减。
3)隔离性:如果存在多个事务,事务之间不应该项目干扰。
4)持久性:事务一旦提交,数据会持久化修改。
2、事务的并发,可能造成脏读、幻读、不可重复读等问题
3、为了解决事务并发造成的问题,所以定义了事务的四种隔离级别。
4、事务的四种隔离级别:读未提交,读已提交,可重复读,串行化。
5、事务的隔离级别是通过什么实现的?锁机制
三、事务的并发造成的问题:
1、脏读:脏读发生在一个事务A读取了被另一个事务B修改,但是还未提交的数据。假如B回退,则事务A读取的是无效的数据(读未提交的数据)。
2、幻读:发生在当两个完全相同的查询执行时,第二次查询所返回的结果集跟第一个查询不相同。
3、不可重复读:一个事务对同一行数据重复读取两次,但是却得到了不同的结果。例如,在两次读取的中途,有另外一个事务对该行数据进行了修改,并提交。
事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。
什么时候会出现幻读?
事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻 子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。
四:事务的隔离级别:
为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
1、读未提交:允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
一个事务可以读取另一个未提交事务的数据。
2、读已提交:允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
3、可重复读:禁止不可重复读取和脏读取,但是有时可能出现幻影数据;读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。
4、串行化:它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
通过选用不同的隔离等级就可以在不同程度上避免前面所提及的在事务处理中所面临的各种问题。所以,数据库隔离级别的选取就显得尤为重要。
大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。
在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。在MySQL中,默认的隔离级别是REPEATABLE-READ(可重复读),并且解决了幻读问题。简单的来说,mysql的默认隔离级别解决了脏读、幻读、不可重复读问题。
不可重复读重点在于update和delete,而幻读的重点在于insert。
五:锁机制:
MySQL中InnoDB引擎的加锁机制(两种数据库引擎)。
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
1、加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得共享锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得排它锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
2、解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。当commit成功后,同时释放锁。
MySQL中锁的种类很多,有常见的表锁和行锁等等:
表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。
行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。
java代码无法控制锁,由数据库引擎来控制。
乐观锁其实无锁,减少锁的开销,只是通过一系列策略来实现锁的功能。
六:乐观锁和悲观锁
针对mysql并发问题的解决方案,避免更新丢失。
为什么需要锁(并发控制)?
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
为了解决这些并发带来的问题。 我们需要引入并发控制机制。保证数据的完整性和可靠性。
1、悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
2、乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。
解决方案:
使用版本号机制(Version)实现,这是乐观锁最常用的一种实现方式。
何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
读用乐观锁,写用悲观锁。
悲观锁的实现,往往依靠数据库提供的锁机制;引擎不同,锁机制不同。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。具体表现形式是什么?省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能。
乐观锁
1)总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
2)version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL代码:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
1.数据库表设计
task 有三个字段,分别是id,value、version
2.实现
1)先读task表的数据(实际上这个表只有一条记录),得到version的值为versionValue
2)每次更新task表中的value字段时,为了防止发生冲突,需要这样操作
update task set value = newValue,version = versionValue + 1 where version = versionValue; 只有这条语句执行了,才表明本次更新value字段的值成功
如假设有两个节点A和B都要更新task表中的value字段值,差不多在同一时刻,A节点和B节点从task表中读到的version值为2,那么A节点和B节点在更新value字段值的时候,都操作 update task set value = newValue,version = 3 where version = 2;,实际上只有1个节点执行该SQL语句成功,假设A节点执行成功,那么此时task表的version字段的值是3,B节点再操作update task set value = newValue,version = 3 where version = 2;这条SQL语句是不执行的,这样就保证了更新task表时不发生冲突。
悲观锁
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。
如何在项目里面使用乐观锁和悲观锁?
1)为数据增加一个版本标识列;
2)每次修改前,先获取版本标识;
3)更新数据的时候,先判断版本号是否一致?如果一致再更新vesion。
乐观锁+重试机制。
本文详细介绍了数据库事务的概念及其核心特性ACID,解释了脏读、不可重复读和幻读等并发问题,并深入探讨了四种隔离级别:读未提交、读已提交、可重复读和串行化,以及乐观锁和悲观锁的实现方式。
1972

被折叠的 条评论
为什么被折叠?



