关于原子性:第三方支付平台提现原子性问题解决方案

在业务逻辑中,常常会碰到提现需要。提现的实现个别分为两个步骤: 扣除余额调用第三方领取接口进行提现(比方微信领取:企业付款到零钱)假如咱们这样写(伪代码): <?phpDB::beginTransaction();try { $member = Member::find($id); $member->money -= $withdrawMoney; $member->save(); $wechat->payment->pay($openid, $withdrawMoney); DB::commit();} catch (\Exception $e) { DB::rollback(); Log::error($e->getMessage());}这样写会有什么问题?当执行commit的时候,因为网络起因,数据库忽然连不上了或者数据库挂了,怎么办?会导致什么结果? 会导致钱付出去了,然而余额没扣。 还有同学可能会有其它疑难,如果数据库操作胜利,接口调用因为网络起因失败了,会怎么样?就咱们下面这段代码而言,如果接口调用失败,并且调用失败后会抛出异样的话,那就会被catch到,try外面甚至都走不到commit,间接rollback了。所以接口调用失败时,不会有原子性问题。怎么样保障提现操作是原子性的,要么扣余额和调接口同时胜利,要么同时失败? 把调微信接口的操作,转化成一个异步工作。 <?phpclass WechatWithdrawJob{ public static function withdraw($openid, $money) { $result = $wechat->payment->pay($openid, $money); if ($result->return_msg == 'SUCCESS') { return true; } return false; }}<?phpDB::beginTransaction();try { $member = Member::find($id); $member->money -= $withdrawMoney; $member->save(); $task = new Task; $task->callback = json_encode([WechatWithdrawJob::class, 'withdraw']); $task->params = json_encode([$openid, $withdrawMoney]); $task->add_time = time(); $task->save(); DB::commit();} catch (\Exception $e) { DB::rollback(); Log::error($e->getMessage());}起一个异步工作生产过程,不停地轮询生产。 ...

September 15, 2021 · 1 min · jiezi

关于原子性:如何实现事务原子性PolarDB原子性深度剖析

简介: 在巍峨的数据库大厦体系中,查问优化器和事务体系是两堵重要的承重墙,二者是如此重要以至于整个数据库体系结构设计中大量的数据结构、机制和个性都是围绕着二者搭建起来的。他们一个负责如何更快的查问到数据,更无效的组织起底层数据体系;一个负责平安、稳固、长久的存储数据,为用户的读写并发提供逻辑实现。咱们明天摸索的主题是事务体系,然而事务体系太过宏大,咱们须要分成若干次的内容。本文就针对PolarDB事务体系中的原子性进行分析。 作者 | 佑熙起源 | 阿里技术公众号 一 前言在巍峨的数据库大厦体系中,查问优化器和事务体系是两堵重要的承重墙,二者是如此重要以至于整个数据库体系结构设计中大量的数据结构、机制和个性都是围绕着二者搭建起来的。他们一个负责如何更快的查问到数据,更无效的组织起底层数据体系;一个负责平安、稳固、长久的存储数据,为用户的读写并发提供逻辑实现。咱们明天摸索的主题是事务体系,然而事务体系太过宏大,咱们须要分成若干次的内容。本文就针对PolarDB事务体系中的原子性进行分析。 二 问题在浏览本文之前,首先提出几个重要的问题,这几个问题或者在接触数据库之前你也已经纳闷过。然而已经这些问题的答案可能只是简略的被诸如“预写日志”,“解体复原机制”等简略的答案答复过了,本文心愿可能更深一步的探讨这些机制的实现及外在原理。 数据库原子性到底是如何保障的?应用了哪些非凡的数据结构?为什么要用?为什么我写入胜利的数据可能被保障不失落?为什么数据库解体后能够残缺的复原进去逻辑上我曾经提交的数据?更进一步,什么是逻辑上已提交的数据?哪一个步骤才算是真正的提交?三 背景1 原子性在ACID中的地位 赫赫有名的ACID个性被提出后这个概念一直的被援用(最后被写入SQL92规范),这四种个性能够大略概括出人们心中对于数据库最外围的诉求。本文要讲的原子性便是其中第一个个性,咱们先关注原子性在事务ACID中的地位。 这是集体对于数据库ACID个性关系的了解,我认为数据库ACID个性其实能够分为两个视角去定义,其中AID(原子、长久、隔离)个性是从事务自身的视角去定义,而C(统一)个性是从用户的视角去定义。上面我会别离谈下本人的了解。 原子性:咱们还是从这些个性的概念登程去探讨,原子性的概念是一个事务要么执行胜利,要么执行失败,即All or nothing。这种特质咱们能够用一个最小的事务模型去定义进去,咱们假如有一个事务,咱们通过一套机制可能实现它真正的提交或回滚,这个目标就达成了,用户只是通过咱们的零碎进行了一次提交,而原子性的重心不在于事务胜利或失败自身;而是保障了事务体系只承受胜利或失败两种状态,而且有相应的策略来保障胜利或失败的物理后果和逻辑后果是统一的。原子性能够通过最小事务单元的个性定义进去,是整个事务体系的基石。持久性:而持久性指的是事务一旦提交后就能够永恒的保留在数据库中。持久性的范畴与视角简直与原子性是统一的,其实也导致了二者在概念和实现上也是严密相连的。二者都肯定意义上保障了数据的统一和可恢复性,而界线便是事务提交的时刻。举例来说,一个数据目前的状态是T,如果某个事务A试图将状态更新到T+1,如果这个事务A失败了,那么数据库状态回到T,这是原子性保障的;如果事务A提交胜利了,那么事务状态变成T+1的那一刻,这个是原子性保障的;而一旦事务状态变成T+1且事务胜利提交,事务曾经完结不再存在原子性,这个T+1的状态就是由持久性负责保障。从这个角度能够推断原子性保障了事务提交前数据的解体复原,而持久性保障了事务提交后的解体复原。隔离性:隔离性同样是定义在事务层面的一个机制,给事务并发提供了某种程度的隔离保障。隔离性的实质是避免事务并发会导致不统一的状态。因为不是本文的重点这里不做详述。一致性:相较于其余几个个性很非凡,一致性的概念是数据库在通过一个或多个事务后,数据库必须放弃在一致性的状态。如果从事务的角度去了解,保障了AID就能够保障事务是可串行、可复原、原子性的,然而这种事务状态的一致性就是真正的一致性吗?毁坏了AID就肯定毁坏C,然而反之AID都保障了C肯定会被保障吗?如果答案是是的话那这个概念就会失去它的意义。咱们能够保障AID来保障事务是统一的,然而是否可能证实事务的统一肯定保证数据的统一呢?另外数据统一这个概念通过事务很难去精确定义,而如果通过用户层面就很好定义。数据统一就是用户认为数据库中数据任何时候的状态是满足其业务逻辑的。比方银行存款不能是正数,所以用户定义了一个非负束缚。我认为这是概念设计者的一个留白,偏向于将一致性视为一种高阶指标。本文次要还是围绕原子性进行,而两头波及到解体复原的话题可能会波及到持久性。隔离性和一致性本文不探讨,在可见性的局部咱们默认数据库具备实现的隔离性,即可串行化的隔离级别。 2 原子性的外在要求 下面讲了很多对于数据库事务个性的了解,上面进入咱们的主题原子性。咱们还是须要拿方才的例子来持续论述原子性。目前数据库的状态是T,当初心愿通过一个事务A将数据状态降级为T+1。咱们探讨这个过程的原子性。 如果咱们要保障这个事务是原子的,那么咱们能够定义三个要求,只有满足了下者,才能够说这个事务是原子性的: 数据库存在一个事务真正胜利提交的工夫点。在这个工夫点之前开启的事务(或者获取的快照)只应该看到T状态,这个工夫点之后开启的事务(或者获取的快照),只应该看到T+1状态。在这个工夫点之前任何时候的解体,数据库都应该可能回到T状态;在这个工夫点之后任何时候解体,数据库都应该能回到T+1状态。留神这个工夫点咱们并没有定义进去,甚至咱们都不能确定2/3中的这个工夫点是不是同一个工夫点。咱们能确定的是这个工夫点肯定存在,否则就没方法说事务是原子性的,原子性确定了提交/回滚必须有一个确定的工夫点。另外依据咱们方才的形容,能够揣测出2中的工夫点,咱们能够定义为原子性位点。因为原子性位点之前的提交咱们不可见,之后可见,那么这个原子性位点对于数据库中其余事务来说就是该事务提交的工夫点;而3中的位点能够定位为持久性位点,因为这合乎持久性对于解体复原的定义。即对于持久性来说,3这个位点后事务曾经提交了。 四 原子性计划探讨1 从两种简略的计划说起 首先咱们从两个简略的计划来谈起原子性,这一步的目标是试图阐明为什么咱们接下来每一步介绍的数据结构都是为了实现原子性必不可少的。 简略Direct IO 构想咱们存在这样一个数据库,每次用户操作都会把数据写到磁盘中。咱们把这种形式叫做简略Direct IO,简略的意思是指咱们没有记录任何数据日志而只记录了数据自身。假如初始的数据版本是T,这样当咱们插入了一些数据之后如果产生了数据解体,磁盘上会写着一个T+0.5版本的数据页,并且咱们没有任何方法去回滚或持续进行后续的操作。这样失败的CASE无疑突破了原子性,因为目前的状态既不是提交也不是回滚而是一个介于两头的状态,所以这是一次失败的尝试。 简略Buffer IO 接下来咱们有了一种新的计划,这种计划叫做简略Buffer IO。同样咱们没有日志,然而咱们退出了一个新的数据结构叫做“共享缓存池”。这样当咱们每次写数据页的时候并不是间接把数据写到数据库上,而是写到了shared buffer pool 中;这样会有不言而喻的劣势,首先读写效率会大大的进步,咱们每次写都不用期待数据页实在的写入磁盘,而能够异步的进行;其次如果数据库在事务未提交前回滚或者解体掉了,咱们只须要抛弃掉shared buffer pool中的数据,只有当数据库胜利提交时,它才能够真正的把数据刷到磁盘上,这样从可见性和解体恢复性上看,咱们看似曾经满足了要求。 然而上述计划还是有一个难以解决的问题,即数据落盘这件事并不像咱们设想的这么简略。比方shared buffer pool中有10个脏页,咱们能够通过存储技术来保障单个页面的刷盘是原子的,然而在这10个页面的两头任何时候数据库都可能解体。继而不管咱们何时决定数据落盘,只有数据落盘的过程中机器产生了解体,这个数据都可能在磁盘上产生一个T+0.5的版本,并且咱们在重启后还是没方法去重做或者回滚。 下面两个例子的论述仿佛注定了数据库没有方法通过不依赖其余构造的状况下保证数据的一致性(还有一种风行的计划是SQLite数据库的Shadow Paging技术,这里不探讨),所以如果想解决这些问题,咱们须要引入下一个重要的数据结构,数据日志。 2 预写日志 + Buffer IO计划 计划总览 咱们在Buffer IO的根底上引入了数据日志这样的数据结构,用来解决数据不统一的问题。 在数据缓存的局部与之前的想法一样,不同的是咱们在写数据之前会额定记录一个xlog buffer。这些xlog buffer是一个有序列的日志,他的序列号被称为lsn,咱们会把这个数据对应的日志lsn记录在数据页面上。每一个数据页页面都记录了更新它最新的日志序号。这一个性是为了保障日志与数据的一致性。 构想一下,如果咱们可能引入的日志与数据版本是完全一致的,并且保证数据日志先于日志长久化,那么不管何时数据解体咱们都能够通过这个统一的日志页复原进去。这样就能够解决之前说的数据解体问题。不管事务提交前或者提交后解体,咱们都能够通过回放日志的计划来回放出正确的数据版本,这样就能够实现解体复原的原子性。另外对于可见性的局部咱们能够通过多版本快照的形式实现。保证数据日志和数据统一并不容易,上面咱们具体讲下如何保障,还有解体时数据如何复原。 事务提交与管制刷脏 WAL日志被设计进去的目标是为了保证数据的可恢复性,而为了保障WAL日志与数据的一致性,当数据缓存被长久化到磁盘时,长久化的数据页对应的WAL日志必须先一步被长久化到磁盘中,这句话论述了管制刷脏的实质含意。 数据库后盾存在这样一个过程叫做checkpoint过程,其周期性的进行checkpoint操作。当checkpoint产生的时候,它会向xlog日志中写入一条checkpoint日志,这条checkpoint日志蕴含了以后的REDO位点。checkpoint保障了以后所有脏数据曾经被刷到了磁盘当中。进行第一次插入操作,此时共享内存找不到这个页面,它会把这个页面从磁盘加载到共享内存中,之后写入本次插入的输出,并且插入一条写数据的xlog到xlog buffer中,将这个表的日志标记从LSN0降级到LSN1。在事务提交的时刻,事务会写入一条事务提交日志,之后wal buffer pool上所有本次事务提交的WAL日志会一并被刷到磁盘上。之后插入第二条数据B,他会插入一条写数据的xlog到xlog buffer中,将这个表的日志标记从LSN1降级到LSN2。同3一样的操作。之后如果数据库失常运行,接下来的bgwriter/checkpoint过程会把数据页异步的刷到磁盘上;而一旦数据库产生解体,因为A、B两条日志对应的数据日志与事务提交日志都曾经被刷到了磁盘上,所以能够通过日志回放在shared buffer pool中从新回放出这些数据,之后异步写入磁盘。 ...

June 2, 2021 · 1 min · jiezi

关于原子性:Java类AtomicReference详解

前言Atomic家族次要是保障多线程环境下的原子性,相比synchronized而言更加轻量级。比拟罕用的是AtomicInteger,作用是对Integer类型操作的封装,而AtomicReference作用是对一般对象的封装。 对AtomicInteger原子性不理解的,能够看这篇:volatile详解 先看个例子// 先简略定义个 User 类@Data@AllArgsConstructorpublic class User { private String name; private Integer age;}// 应用 AtomicReference 初始化,并赋值public static void main( String[] args ) { User user1 = new User("张三", 23); User user2 = new User("李四", 25); User user3 = new User("王五", 20); //初始化为 user1 AtomicReference<User> atomicReference = new AtomicReference<>(); atomicReference.set(user1); //把 user2 赋给 atomicReference atomicReference.compareAndSet(user1, user2); System.out.println(atomicReference.get()); //把 user3 赋给 atomicReference atomicReference.compareAndSet(user1, user3); System.out.println(atomicReference.get());}输入后果如下: User(name=李四, age=25)User(name=李四, age=25)解释compareAndSet(V expect, V update)该办法作用是:如果atomicReference==expect,就把update赋给atomicReference,否则不做任何解决。 ...

May 13, 2021 · 1 min · jiezi