支持串行隔离级别_从0到1理解数据库事务(上):并发问题与隔离级别

最近准备写一篇关于Spanner事务的分享,所以先分享一些基础知识,涉及ACID、隔离级别、MVCC、锁,由于太长,只好拆分成上下两篇:

  • 上:并发问题与隔离级别

主要讲事务所要解决的问题、思路,先理解为什么需要事务以及事务并发控制中面临的问题。

  • 下:隔离级别实现——MVCC与锁

隔离性是为了更好地做到并发控制,事务的并发表现会对业务有直接影响,所以这篇会详细讲如何实现隔离,主要是讲两种主流技术方案——MVCC与锁,理解了MVCC与锁,就可以举一反三地看各种数据库并发控制方案,并理解每种实现能解决的问题以及需要开发者自己注意的并发问题,以更好支撑业务开发。

文章开始前先给一个小思考,考虑一个情况:
像下面这样实现User提现100元,是否一定不会出问题?
Start Transaction
SELECT balance FROM users WHERE user_name=x; (此次读取在Transaction中)
在代码中判断balance是否大于等于100
如果小于100元,End Transaction并且返回余额不足提现失败
如果大于等于100元,则 UPDATE users SET balance = balance - 100 WHERE user_name=x; 然后Commit Transaction,返回提现成功

如果你已经很理解数据库事务了,一定知道什么情况有问题,以及为什么出现这个问题,这篇文章对你太入门,不用继续看。如果不太清楚,那希望你看完上、下两篇就非常理解了,否则就是我写得太烂。

一、重新理解 ACID

1. 数据操作中面临的问题

技术中的所有方案必定是为了解决特定问题,先理解问题再看方案,学起来更简单、理解更深入,所以先从数据库面临的问题说起。

首先,要理解为什么数据库会有事务的需求,先理解数据库要解决的根本问题不是存储,存储问题已经被文件系统解决了,数据库的目的是如何帮助开发者更可靠、更快速、更便利地使用存储,更好地帮助开发者完成业务,业务中一个高频需求是:有一批连续的操作,这一批操作要么全部成功,要么就可以像没有发生过一样,不要由于部分未成功而导致脏数据产生。如果没有事务,我们处理用户下单的业务场景,就要超级多的代码去handle各种错误、清理各种脏数据、避免可能的bug,比如下单成功却由于数据库宕机导致没有扣款。为了提高开发效率、降低开发成本,就需要数据库能提供一种保证:将一组操作看作一个单元,这一单元可以全部成功,在部分失败的情况下,可以完全回滚,就像没有发生,这一组操作称为事务(Transaction)

但是仅仅做到上面那一点是不够的,因为这一个简单需求,其实引入了另一个问题,请注意重点——“一组操作”,事务中可能存在着多个独立操作,他们组合为一组操作,理解多线程编程的同学一定会马上想到,这就会出现经典的并发问题,多个事务间如果不进行并发控制,就会产生各种意外结果,这不是使用者想要的。

总结一下,数据操作中面临的问题:
如何将一组操作看作一个整体,要么全部成功,要么全部回滚。
如何在满足上一条需求的情况下,能够对它进行并发控制,保证不要出现意外结果。

2. 我们需要什么:ACID

ACID 是为了解决上述问题所总结出,为保证事务是正确可靠的,所必须具备的四个特性:

1. 原子性(Atomicity)

事务中的原子性是一个常常被大家误解的特性,因为这个原子性的意思和我们通常语境下的原子性不太一样,大多数时候原子性是指一条不可再分割、不会被中断影响的指令,比如读取一个内存地址的值、将值写回内存地址、redis的SETNX(set if not exists),这些操作都符合我们常说的原子性。

可是事务中的原子性,并不是指事务具有不被中断影响的特点,它仅仅是指,事务中的所有操作应该被看作不可分割的一组指令,任何一个指令不能独立存在,要么全部成功执行,要么全部不发生(也就是回滚)

还有很多同学对这里所说的“成功执行”有误解,成功执行是指数据库层面的,而不是业务层面的,举个例子,客户购买商品A,可是在购买时,商家刚好下架了商品,那么此时执行 update products where product_id=A and status=销售中 ,由于product的status已经变成“下架”,导致被更新的行数为0,这个算成功执行吗?算!数据库不报错、不宕机、正常运行就是成功,更新行数为0是数据库的正常返回结果,这在业务上是失败,在数据库层面是成功,这种情况数据库不会执行回滚,需要程序员判断更新行数,如果为0,手动回滚。

如果数据库由于硬件或者系统问题发生宕机、报错,这样才算是指令执行失败,此时数据库会重试或者直接回滚,然后将错误返回给开发者。

原子性不止为开发者保证了事务的可靠性(不会因为数据库出错而产生脏数据),还能让开发者手动回滚,提供了业务的便利性。

2. 一致性(Consistency)

这个名词也是相当令人困惑,与数据库主从复制中所说的“一致性”不同,主从复制的一致性是指多个副本间是否完成同步、数据相同,而这里的一致性是指事务是否产生非预期中间状态或结果。比如脏读和不可重复读,产生了非预期中间状态,脏写与丢失修改则产生了非预期结果。一致性实际上是由后面的隔离性去进一步保证的,隔离性达到要求,则可以满足一致性。也就是说,隔离不足会导致事务不满足一致性要求,所以务必理解各个隔离级别,才能少写Bug。

3. 隔离性(Isolation)

简单来说,隔离性就是多个事务互不影响,感觉不到对方存在,这个特性就是为了做并发控制。在多线程编程中,如果大家都读写同一块数据,那么久可能出现最终数据不一致,也就是每条线程都可能被别的线程影响了。按理说,最严格的隔离性实现就是完全感知不到其他并发事务的存在,多个并发事务无论如何调度,结果都与串行执行一样。为了达到串行效果,目前采用的方式一般是两阶段加锁(Two Phase Locking),但是读写都加锁效率非常低,读写之间只能排队执行,有时候为了效率,原则是可以妥协的,于是隔离性并不严格,它被分为了多种级别,从高到低分别为:

  • ⬇️可串行化(Serializable)
  • ⬇️可重复读(Read Repeatable)
  • ⬇️已提交读(Read Committed)
  • ⬇️未提交读(Read Uncommitted)

每一个级别都只是指导标准,每个数据库对其的实现都有差异,有的数据库在Read Committed级别时,就已经实现了Read Repeatable的效果,有的数据库干脆不提供Read Uncommitted级别。

在隔离级别为Serializable时,就会感觉到事务像一个完完全全的原子操作,不被任何中断、并发所影响。

很多开发者理解的事务可能就在Serializable级别,大家误以为事务都是可串行化的,其实并不是,大多数的数据库默认隔离级别都不是可串行化,大多数在Read Repeatable或者Read Committed,要是按照可串行化的思维去编程,却用着低于可串行化的隔离级别,就很容易写出导致数据在业务层面不一致的代码,所以开发者一定要理解各个隔离级别及其原理,更好地支撑业务开发,下面会仔细地讲隔离级别及其实现。

4. 持久性(Duration)

这是ACID中最好理解的,即事务成功提交后,对数据的修改永久的,即使系统发生故障,也不会丢失,这里所说的故障,也只是一般错误比如宕机、系统Bug、断电,如果是硬盘损毁,那就没办法,数据一定会丢失。

二、并发问题与隔离级别

在讨论各个隔离级别的实现之前,先看一下在事务并发执行时,隔离不足会导致的问题。

脏写(Dirty Write)

还未提交的事务写了另一个未提交事务所写过的数据,称为脏写,比如:

两个并发执行的事务A、B,A写了x,在A还未提交前,B也写了x,然后A提交,此时虽然B还没有提交,但是A也会发现自己写的x不见了。

eff444b6e411c63b7ddda70b5ffd93a2.png

很多地方用“覆盖”去形容脏写,但是我觉得不太适合,因为覆盖暗示了一种先后链条,某个事务写了数据,在昨天就提交了,今天有事务来写同一个数据,可以称之为覆盖,昨天的数据成为历史,但这不是脏写,所以更适合的形容可能是“擦除”,事务发现自己的提交被别人擦除,好像不存在。

脏写是事务一定不允许发生的,所以不管是哪个隔离级别都一定不允许脏写

脏读(Dirty Read)

由于事务的可回滚特性,因此commit前的任何读写,都有被撤销的可能,假如某个事物读取了还未commit事务的写数据,后来对方回滚了,那么读到的就是脏数据,因为它已经不存在了。

f794fc065af53532f11a499cd99f8f80.png

避免脏读可以采用加锁或者快照读的解决方案。在已提交读(Read Committed)级别就可以避免脏读,因为读到的一定是已经Commit的数据。在业务开发中,虽然有未提交读(Read Uncommitted),但是几乎是没有人会用的,读到脏数据一般对业务是很大的伤害,所以有的数据库干脆都不支持未提交读,比如PostgreSQL。

不可重复读(Non-Repeatable Read)

事务A读取一个值,但是没有对它进行任何修改,另一个并发事务B修改了这个值并且提交了,事务A再去读,发现已经不是自己第一次读到的值了,是B修改后的值,就是不可重复读。

简单来说就是第一次读的值,啥都没做,下次读它也有可能发生变化。

e13deef6531f4c22d2525eccb592b4b7.png

一般数据库使用MVCC,在事务的第一条语句开始时生成Read View,事务之后的所有读取,都是基于同一个Read View,以此避免不可重复读问题。

幻读(Phantom)

与不可重复读非常类似,事务A查询一个范围的值,另一个并发事务B往这个范围中插入了数据并提交,然后事务A再查询相同范围,发现多了一条记录,或者某条记录被别的事务删除,事务A发现少了一条记录。

6d34464fd3857d7d3b0a88a80d3cb6ac.png

幻读容易与不可重复读混淆,区别它们只需要记住不可重复读面向的是“同一条记录”,而幻读面向的是“同一个范围”

MVCC虽然使用快照的方式解决了不可重复读,但是还是不能避免幻读,幻读需要通过范围锁解决,可能大家会觉得很奇怪,为什么快照读无法避免幻读,这个会在下一篇文章中详细讲。

SQL标准中有对于各个隔离级别所允许出现的问题作出规定:

1129847684a9e12e2d978a1c8bf122e1.png

除了以上4个问题外,下面还有3个问题,更偏向业务层面,不过也是由于隔离不足引起的:

读偏斜(Read Skew)

Skew可以理解为不一致,因此读偏斜可以理解为读结果违反业务一致性,比如X、Y两个账户余额都为50,他们总和为100,事务A读X余额为50,然后事务B从X转账50到Y然后提交,事务A在B提交后读Y发现余额为100,那么它们总和变成了150,此时违反业务一致性。

ec9a3932cdb65a691c17c857a7fdaeac.png

写偏斜(Write Skew)

写偏斜可以理解为事务commit之前写前提被破坏,导致写入了违反业务一致性的数据,网上有个很好的简称为写前提困境,也就是读出某些数据,作为另一些写入的前提条件,但是在提交前,读入的数据就已被别的事务修改并提交,这个事务并不知道,然后commit了自己的另一些写入,写前提在commit前就被修改,导致写入结果违反业务一致性。

写偏斜发生在写前提与写入目标不相同的情境下

这是业务开发中最容易出错地方,如果开发者不太理解隔离级别,也不知道目前使用的是哪个隔离级别,很可能写出有写偏斜的代码,造成业务不一致。

举个例子:

信用卡系统对不同等级的会员有积分加成,3级会员则每次都3倍积分,同时,会有定时任务检查当积分不满足要求时,就会降级。

首先,会员进行了刷卡消费,此时要计算积分,开启了事务A,读到会员等级为3,与此同时定时任务也开始了,读到会员积分为2800,已经不满足3000分应该降级为2级,然后将会员等级降级为2并且commit,由于事务A读到的等级为3,它还是按照3倍积分为会员增加了积分,会员赚了,多亏那个程序员不理解他使用的事务隔离级别,出现了业务不一致。

70282402ec0839b17e3c34cc19977a23.png

丢失更新(Lost Updates)

由于未提交事务之间看不到对方的修改,因此都以一个旧前提去更新同一个数据,导致最后的提交结果是错误值。

假设有支付宝账户X,余额100元,事务A、B同时向X分别充值10元、20元,最后结果应该为130元,但是由于丢失更新,最后是110元。

2463b552619d6a30ae74e6e4e06d39c2.png

丢失更新与写偏斜很相似,都是由于写前提被改变,他们区别是,丢失更新是在同一个数据的最终不一致,而写偏斜的冲突不在同一个数据,在不同数据中的最终不一致

这一篇讲到的所有问题都会在下一篇讲隔离级别实现中得到解决,理解隔离级别实现,有助于选择合适的隔离级别,或者在代码层面有意识地避免隔离级别不足所带来的问题。

参考资料

  • 《MySQL 是怎样运行的:从根儿上理解 MySQL》
  • 《数据库事务处理的艺术:事务管理与并发控制》
  • 《数据库事务、隔离级别和锁》(博客)
  • 《SQL隔离级别》(博客)
  • 《开发者都应该了解的数据库隔离级别》(博客)
  • 《A beginner’s guide to read and write skew phenomena》(博客)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/563825.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何发布接口_Devops下的接口全生命周期管理与测试

什么是devops?随着时间的推移,devops的定义也在不断的演进。对于其定义可能出现千人千面,但从核心观点,整体业界还是保持着一致的认识。DevOps不是单一的技术或者工具,甚至不只是一个流程,而是包含应用设计…

查看mysql日志post_(转)MySQL 日志组提交

原文:https://jin-yang.github.io/post/mysql-group-commit.html组提交 (group commit) 是为了优化写日志时的刷磁盘问题,从最初只支持 InnoDB redo log 组提交,到 5.6 官方版本同时支持 redo log 和 binlog 组提交,大大提高了 MySQL 的事务处…

like语句太慢 sqlserver_SQLServer找出执行慢的SQL语句

SELECT(total_elapsed_time / execution_count)/1000 N平均时间ms,total_elapsed_time/1000 N总花费时间ms,total_worker_time/1000 N所用的CPU总时间ms,total_physical_reads N物理读取总次数,total_logical_reads/execution_count N每次逻辑读次数,total_logical_reads N逻辑…

苹果cms10自适应模板_哪里有苹果cms10自适应模板?

1,苹果CMSv10大图轮播高端大气自适应视频网站模板源码苹果cms10自适应模板下载地址:https://www.mytheme.cn/maccms/54.html第一款大图宽屏的海报轮播幻灯样式,宽屏模板支持DIY扩展自适应影视模板苹果cms10自适应模板苹果cms10自适应模板2&am…

python实现进程通信_python进程间的通讯实现

1:进程间通讯的方法:apply_async()非阻塞式通讯 apply()阻塞式通讯2:使用Queue实现对Process创建的进程间通讯,Queue本身是一个消息队列程序,Queue常用方法:Queue.qsize():返回当前消息队列的消息数量Q…

vscode php formatter mac配置_Mac上配置Vs code时,遇到的几个“坑”!

在写一些简单的程序时,你喜欢用什么编译器呢?之前我一直用的是sublime,它打开的速度快,占用的内存小。但是有个麻烦的地方,配置的时候要花一些时间,如果你的网不好还时不时给你来个timeout。最近在Youtube上…

模为2的逆元是什么_两种求模m逆元的方法

在a|b(a能整除b)的前提下&#xff0c;计算(b/a)mod m的时候转化为 计算(b*x)mod m ; 这时的x就是a的逆元(a模m的逆元)&#xff1b;此时x满足 (a*x mod m 1)&#xff1b; 这个x的求法有一下两种&#xff1a;1)扩展欧几里得算法求解 a*xm*y1; 因为 a*x mod m 1 <> a…

java值栈_Struts2 中的值栈是什么?

7.1值栈7.1.1值栈是什么简单的说&#xff1a;值栈是对应每一个请求对象的轻量级的内存数据中心。Struts2中一个很激动人心的特性就是引入了值栈&#xff0c;在这里统一管理着数据&#xff0c;供Action、Result、Interceptor等Struts2的其他部分使用&#xff0c;这样一来&#x…

java try的用法_Java中try、catch的使用方法

Java中有两种处理异常的方式&#xff0c;分别是用throws抛出异常、用try、catch捕获异常。try-catch在Javatry-catch语句的语法格式&#xff1a;try{//代码块}catch(Exception1 e){//抛出异常后所要进行的操作}当try语句当中捕获到异常时&#xff0c;会将异常抛出到catch语句中…

java not a jpeg file_javax.imageio.IIOException: Not a JPEG file: starts with 0x47 0x49

java处理图片时出现异常javax.imageio.IIOException: Not a JPEG file: starts with 0x47 0x49at com.sun.imageio.plugins.jpeg.JPEGImageReader.readImageHeader(Native Method)at com.sun.imageio.plugins.jpeg.JPEGImageReader.readNativeHeader(Unknown Source)at com.sun…

java if用法_java中if语句的写法

if语句if 语句的语法如下&#xff1a;if(布尔表达式){//如果布尔表达式为true将执行的语句}如果布尔表达式的值为 true&#xff0c;则执行 if 语句中的代码块&#xff0c;否则执行 if 语句块后面的代码。免费视频教程推荐&#xff1a;java视频教程if...else语句if 语句后面可以…

java子类和父类实例_java中父类与子类之间的转换示例

java中父类与子类之间的转换示例有以下三点&#xff1a;示例一父类强制转子类pre class"brush:php;toolbar:false">Father f new Son();Son s (Son)f;//可以创建一个父类的实例&#xff0c;想要强制把父类对象转换成子类的&#xff0c;不行&#xff01;通俗的想…

java 原子类能做什么_死磕 java原子类之终结篇(面试题)

概览原子操作是指不会被线程调度机制打断的操作&#xff0c;这种操作一旦开始&#xff0c;就一直运行到结束&#xff0c;中间不会有任何线程上下文切换。原子操作可以是一个步骤&#xff0c;也可以是多个操作步骤&#xff0c;但是其顺序不可以被打乱&#xff0c;也不可以被切割…

java对docker_如何在docker中运行java程序

吃鸡游戏创建一个redis docker容器首先&#xff0c;我们先为redis创建一个DockerfileFROM ubuntu:12.10RUN apt-get updateRUN apt-get -y install redis-serverEXPOSE 6379ENTRYPOINT ["/usr/bin/redis-server"]现在你需要通过Dockerfile创建一个镜像&#xff0c;将…

java canvas 画图片_canvas画布——画八卦图

浏览器支持Internet Explorer 9、Firefox、Opera、Chrome 以及 Safari 支持 arc() 方法。注释&#xff1a;Internet Explorer 8 或更早的浏览器不支持 元素。定义和用法arc() 方法创建弧/曲线(用于创建圆或部分圆)。提示&#xff1a;如需通过 arc() 来创建圆&#xff0c;请把起…

java多线程编程—高级主题_Java day20 高级编程【第一章】Java多线程编程

【第一章】Java多线程编程一.进程与线程多个时间段会有多个程序依次执行&#xff0c;但是同一时间点只有一个进程执行线程是在进程基础之上划分的更小的程序单元 &#xff0c;线程是在进程基础上创建并且使用的&#xff0c;所以线程依赖于进程的支持&#xff0c;但是来讲&#…

java object数组转实体类_详解Java中String JSONObject JSONArray List实体类转换

JSON使用阿里的fastJson为依赖包gradle依赖管理如下&#xff1a;compile group: "com.alibaba", name: "fastjson", version:"1.2.41"1、String转JSONObject前言&#xff1a;String 是JSONObject格式的字符串eg:JSONObject jSONObject JSONObje…

e x泰勒 java_maven project

最近没事了玩一下maven&#xff0c;使用maven管理工程中的依赖包非常的方便。建立maven web工程的时候开始不知道怎么用tomcat来调试&#xff0c;总是使用mave的tomcat插件发布了后来调试&#xff0c;觉得非常的麻烦&#xff0c;网上找了点材料&#xff0c;看看maven web工程不…

java定义说话方法_类定义的基本形式_Java语言程

类定义的基本形式_Java语言程4&#xff0e;2&#xff0e;1 类定义的基本形式前面一节我们已经了解了类和对象的概念&#xff0c;然而在实际编程过程中&#xff0c;该如何定义一个类以及类中的对象呢&#xff1f;在Java语言中&#xff0c;用户自己可以定义一个类&#xff0c;作…

java创建线程几种_java中创建线程有几种方式

详细内容线程的创建方式1、继承Thread类实现多线程2、覆写Runnable()接口实现多线程&#xff0c;而后同样覆写run()。推荐此方式3、使用Callable和Future创建线程相关视频教程推荐&#xff1a;java学习视频实例如下&#xff1a;1、继承Thread类实现多线程/** 继承Thread类创建线…