1.1    创建嵌套事务

在之前的示例中,每个用到事务的方法都是各自在其内部单独创建事务,并且事务所涉及的变动也都是各自独立提交的。但如果我们想要将多个方法里的事务调整成一个统一的原子操作的时候,上述做法就无能为力了,所以我们需要使用嵌套事务来实现这一目标。

通过使用嵌套事务,所有被主控函数调用的那些函数所创建的事务都会默认被整合到主控函数的事务中。除此之外,Akka/Multiverse还提供 了很多其他配置选项,如新隔离事务(new isolated transactions)等。总之,使用了嵌套事务之后,只有位于最外层的主控函数事务提交时,其内部所做的变更才会被提交。在具体使用时,为了保证所 有嵌套事务能够作为一个整体成功完成,我们需要保证所有函数都必须在一个可配置的超时范围内做完。

我们在4.6节中通过加锁方式实现的AccountService的transfer()函数将会受益于嵌套事务。因为这个版本的transfer()函 数需要按自然顺序对所有账户排序并显式地对锁进行管理。STM将为我们消除所有这些负担。下面我们会首先在Java中用嵌套事务重新实现这一示例,然后再 来看一下该示例在Scala中是如何实现的。

在Java中使用嵌套事务

现在让我们开始对Account类进行事务化的改造吧。首先我们需要把保存账户余额的变量balance改成托管引用,下面我们就来定义这个字段以及该字段的getter函数。

public class Account {
final private Ref<Integer> balance = new Ref<Integer>();
public Account(int initialBalance) { balance.swap(initialBalance); }
public int getBalance() { return balance.get(); }

在构造函数中,我们用Ref的swap()函数将给定的数量设置成balance的初始值。由于swap()函数运行在自己独立的事务中,所以我们 就无需再创建额外的事务了(同时我们假设调用者也不会为这个操作创建额外的事务)。getBalance()函数的情况与之类似,就不再赘述了。

由于deposit()函数需要对balance进行先读后写的操作,所以该函数内的所有操作需要整体封装到一个事务里运行。下面的代码为我们展示了如何将这两个操作封装到一个独立事务中的方法。

public void deposit(final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
System.out.println("Deposit " + amount);
if (amount > 0) {
balance.swap(balance.get() + amount);
return true;
}
throw new AccountOperationFailedException();
}
}.

基于同样的理由,我们需要把withdraw()函数里的所有操作也封装到一个独立的事务中。

public void withdraw(final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
int currentBalance = balance.get();
112 • Chapter 6. Introduction to Software Transactional Memory
if (amount > 0 && currentBalance >= amount) {
balance.swap(currentBalance - amount);
return true;
}
throw new AccountOperationFailedException();
}
}.execute();
}
}

如果运行过程中有异常抛出,则事务将会强制失败。所以当账户内余额不足或存款/取款操作输入了非法参数时,我们就可以利用这一点来表示操作失败。相当简单,是吧?从此我们就可以不用再担心同步、加锁、死锁等令人烦恼的问题了。

现在到了该浏览一下执行转账操作的AccountService类的时候了,让我们首先来看一下其中的transfer()函数(校注:java中应该叫方法)

public class AccountService {
public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
System.out.println("Attempting transfer...");
to.deposit(amount);
System.out.println("Simulating a delay in transfer...");
try { Thread.sleep(5000); } catch(Exception ex) {}
System.out.println("Uncommitted balance after deposit $" +
to.getBalance());
from.withdraw(amount);
return true;
}
}.execute();
}

在这个示例中,我们会将多个事务置于相互冲突的环境中,以此来演示嵌套事务的行为并帮助你加深对嵌套事务的理解。Transfer()函数中的所有 操作都是在同一个事务中完成的。作为转账过程的一部分,我们首先将钱存到目标账户中。紧接着,在经过一个为引入事务冲突而专门设置的延时之后,我们将钱从 源账户中划走。我们希望当且仅当从源帐户划款成功之后,向目标账户存款的操作才能够成功,这也是我们这个事务所要完成的目标。

我们可以通过打印balance的值来观察转账操作是否成功。如果有一个方便的函数来调用transfer()函数,处理下异常,并在最后打印一下balance的值就更好了,下面就让我们动手写一个吧:

public static void transferAndPrintBalance(
final Account from, final Account to, final int amount) {
boolean result = true;
try {
new AccountService().transfer(from, to, amount);
} catch(AccountOperationFailedException ex) {
result = false;
}
System.out.println("Result of transfer is " + (result ? "Pass" : "Fail"));
System.out.println("From account has $" + from.getBalance());
System.out.println("To account has $" + to.getBalance());
}

最后我们还需要一个main()函数来让整个示例运转起来。

public static void main(final String[] args) throws Exception {
final Account account1 = new Account(2000);
final Account account2 = new Account(100);
final ExecutorService service = Executors.newSingleThreadExecutor();
service.submit(new Runnable() {
public void run() {
try { Thread.sleep(1000); } catch(Exception ex) {}
account2.deposit(20);
}
});
service.shutdown();
transferAndPrintBalance(account1, account2, 500);
System.out.println("Making large transfer...");
transferAndPrintBalance(account1, account2, 5000);
}
}

在main函数中,我们创建了两个账户,并在一个单独的线程中从第二个账户里取走$20。与此同时,我们还启动了一个在账户之间转账的事务。由于这 些操作都会影响到公共实例(即两个账户——译者注),所以这种做法将导致两个事务(存$20的事务和转账$500的事务——译者注)产生冲突。于是只有一 个事务能够顺利完成,而另一个将会重做。最后,我们会启动一个超出源账户余额的转账操作,以此来演示存款和取款这两个相互关联的事务通过嵌套事务的方式在 转账过程中实现了原子性的操作。下面让我们通过输出结果来观察事务的行为:

Attempting transfer...
Deposit 500
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Deposit 20
Uncommitted balance after deposit $600
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Uncommitted balance after deposit $620
Result of transfer is Pass
From account has $1500
To account has $620
Making large transfer...
Attempting transfer...
Deposit 5000
Simulating a delay in transfer...
Uncommitted balance after deposit $5620
Result of transfer is Fail
From account has $1500
To account has $620

输出结果起始处的重试操作让人看起来有些摸不着头脑。这个非预期的重试是由Multiverse对于单个对象上的只读事务的默认优化造成的。虽然有 两种方法可以重新配置这一行为,但修改了之后可能会对性能造成影响。请参阅Akka/Multiverse文档来进一步了解变更这一配置所造成的影响。

在本例中,向帐户2存$20的操作会先完成。而与此同时,从账户1向账户2的转账事务则处于模拟的延迟当中。当转账事务重新恢复运行并察觉到其涉及 的对象发生了变化时,该事务将悄悄地回滚并重做。如果事务在运行过程中一直出现内部数据有变化的情况,则该事务会不断重做直至成功或超时退出为止。本例中 的转账事务是最终成功了的,帐户余额的变化充分地反映了这一结果——账户1转出了$500,而账户2则从并发的存款和转账操作中总共获取了$520。

本例的最后一个操作是从账户1向账户2转$5000。在这个事务中,存款操作顺利完成了,但事务能否最终成功还是要看取款操作的结果。不出所料,取款动作由于账户余额不足而失败并抛了异常。随后,之前的存款动作被回滚,系统最终保证了账户余额数据不受事务失败的影响。

再次声明,在事务中打印信息和插入延时都不是好习惯,我在本例中这样用是为了使你能够更好地观察事务的运行顺序和重做行为,在实际工作中请最好不要 在事务代码里打印消息或打日志。请记住,事务是不应该有任何副作用的。如果事务中确实需要包含有副作用的操作,我们可以将这些代码放到后面将会提到的后置 提交(post-commit)handler里面去。

我可以拍胸脯向你保证,使用事务绝对可以替你分担大部分并发编程方面的烦恼。下面就让我们通过一组对比来看看事务到底效用几何。让我们回顾一下4.6节中我们用加锁方式实现的转账函数transfer(),为方便起见我将代码列在下面:

public boolean transfer(
final Account from, final Account to, final int amount)
throws LockException, InterruptedException {
final Account[] accounts = new Account[] {from, to};
Arrays.sort(accounts);
if(accounts[0].monitor.tryLock(1, TimeUnit.SECONDS)) {
try {
if (accounts[1].monitor.tryLock(1, TimeUnit.SECONDS)) {
try {
if(from.withdraw(amount)) {
to.deposit(amount);
return true;
} else {
return false;
}
} finally {
accounts[1].monitor.unlock();
}
}
} finally {
accounts[0].monitor.unlock();
}
}
throw new LockException("Unable to acquire locks on the accounts");
}

你可以将上述代码与去掉了延时和log输出的事务版本进行比较:

public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
to.deposit(amount);
from.withdraw(amount);
return true;
}
}.execute();
}

旧版本的代码既要考虑加锁的问题又要顾及加锁的顺序,所以很容易出错。代码越多越容易出问题,这是显而易见的道理。在新版本中,我们显著地降低了代 码量和复杂度。这让我想起了C.A.R.Hoare的名言:“这世界上有两种构建软件设计的方法。一种方法是使其足够简单以至于不存在明显的缺陷。而另一 种方法是使其足够复杂以至于无法看出有什么毛病” 。只有让代码更少、结构更简单,我们才能将更多的时间投入到程序逻辑的设计开发中去。

在Scala中使用嵌套事务

从上例中我们可以看到,使用了嵌套事务的Java版转账函数是非常简洁的。然而,虽然事务的使用让我们得以去除Java中那些用于同步的冗余代码, 但还是会有一些由于Java语法需要而存在的一些额外代码。正如我们下面所看到的那样,Scala的优雅和强大的表达能力使其在代码清晰简洁方面更胜一 筹。下面就是Scala版的Account类:

class Account(val initialBalance : Int) {
val balance = Ref(initialBalance)
def getBalance() = balance.get()
def deposit(amount : Int) = {
atomic {
println("Deposit " + amount)
if(amount > 0)
balance.swap(balance.get() + amount)
else
throw new AccountOperationFailedException()
}
}
def withdraw(amount : Int) = {
atomic {
val currentBalance = balance.get()
if(amount > 0 && currentBalance >= amount)
balance.swap(currentBalance - amount)
else
throw new AccountOperationFailedException()
}
}
}

Scala版本的Account是逻辑直接从Java版本翻译过来的、但代码风格又带有Scala和Akka简洁优雅特征的一种实现。在Scala版本的AccountService中我们也可以看到同样的优点

object AccountService {
def transfer(from : Account, to : Account, amount : Int) = {
atomic {
println("Attempting transfer...")
to.deposit(amount)
println("Simulating a delay in transfer...")
Thread.sleep(5000)
println("Uncommitted balance after deposit $" + to.getBalance())
from.withdraw(amount)
}
}
def transferAndPrintBalance(
from : Account, to : Account, amount : Int) = {
var result = "Pass"
try {
AccountService.transfer(from, to, amount)
} catch {
case ex => result = "Fail"
}
println("Result of transfer is " + result)
println("From account has $" + from.getBalance())
println("To account has $" + to.getBalance())
}
def main(args : Array[String]) = {
val account1 = new Account(2000)
val account2 = new Account(100)
actor {
Thread.sleep(1000)
account2.deposit(20)
}
transferAndPrintBalance(account1, account2, 500)
println("Making large transfer...")
transferAndPrintBalance(account1, account2, 5000)
}
}

与Java版本一样,Scala版本的AccountService同样会将事务置于相互冲突的环境之下。所以毫无悬念,其输出结果也与Java版本完全相同:

Attempting transfer...
Deposit 500
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
118 • Chapter 6. Introduction to Software Transactional Memory
Deposit 20
Uncommitted balance after deposit $600
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Uncommitted balance after deposit $620
Result of transfer is Pass
From account has $1500
To account has $620
Making large transfer...
Attempting transfer...
Deposit 5000
Simulating a delay in transfer...
Uncommitted balance after deposit $5620
Result of transfer is Fail
From account has $1500
To account has $620

前面我们已经比较过用Java实现的加锁同步版本和嵌套事务版本(如下所示)的转账函数

public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
to.deposit(amount);
from.withdraw(amount);
return true;
}
}.execute();
}

现在让我们将之与Scala版本进行一下比较:

def transfer(from : Account, to : Account, amount : Int) = {
atomic {
to.deposit(amount)
from.withdraw(amount)
}
}

从上面的对比中我们可以清晰地看到,Scala版本的代码除了核心逻辑之外没有任何冗余。这又让我想起了Alan Perlis的名言:“如果用某种编程语言写代码时还需要注意一些与核心逻辑无关的东西,那么这个语言就是低级语言。”

截至目前,我们已经学习了如何用Akka创建事务以及如何组合嵌套事务,但我们才刚上路呢。下面我们将一起了解一下在Akka中如何对事务进行配置。

文章转自 并发编程网-ifeve.com

软件事务内存导论(五)创建嵌套事务相关推荐

  1. STM 软件事务内存——本质是为提高并发,通过事务来管理内存的读写访问以避免锁的使用...

    对Java程序员来说,我们对面向对象的编程(OOP)自然都是烂熟于胸的,但语言也极大地影响了我们构建面向对象应用程序的方式.(现在的OOP已经和Alan Kay当初创造这个词时候的初衷大不相同了,他的 ...

  2. Java并发编程实战~软件事务内存

    很多同学反馈说,工作了挺长时间但是没有机会接触并发编程,实际上我们天天都在写并发程序,只不过并发相关的问题都被类似 Tomcat 这样的 Web 服务器以及 MySQL 这样的数据库解决了.尤其是数据 ...

  3. 软件调试学习笔记(五)—— 软件断点内存断点

    软件调试学习笔记(五)-- 软件断点&内存断点 调试的本质 软件断点 软件断点的执行流程 分析INT 3执行流程 实验:处理软件断点 内存断点 内存断点的执行流程 实验:处理内存断点 调试的本 ...

  4. Spring事务配置的五种方式 说明

    Spring事务配置的五种方式  [转 http://blog.csdn.net/hjm4702192/article/details/17277669] Spring配置文件中关于事务配置总是由三个 ...

  5. 计算机组成和导论,计算机科学导论五第章计算机组成

    计算机科学导论五第章计算机组成 (58页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 14.90 积分 第5章 计算机组成 计算机导论 计算机硬件系统的组 ...

  6. 导论 计算机组成 ppt,计算机科学导论五章计算机组成.ppt

    计算机科学导论五章计算机组成 CPU cpu风扇 内存 显卡 主板 声卡 外存储器 1.磁盘 磁盘内部组成 磁盘与磁盘驱动器是封装在一起的. 磁盘片 读写磁头 外存储器 2.光盘(Compact Di ...

  7. 0324的学习笔记----里面最重要的就是一个tom猫的动画,和涉及到的内存问题(创建imageview的两种方式,imagenamed就会形成缓存,占用很多内

    还是可以自己对着视频,或者自己有空的时候在做一遍,把按钮做全面的,比较有意思. 看视频的时间是:2015.11.2日上午. 0324: 01-作业-QQ登陆界面 (1) 键盘的退出:[self.vie ...

  8. python 申请内存空间、用于创建多维数组_python 申请内存空间,用于创建多维数组的实例...

    以三维数组为例 先申请1个一维数组空间: mat = [None]*d1 d1是第一维的长度. 再把mat中每个元素扩展为第二维的长度: for i in range(len(mat)): mat[i ...

  9. SAP QM 使用QP01事务代码真的不能创建含有Multiple Specification的检验计划

    SAP QM 使用QP01事务代码真的不能创建含有Multiple Specification的检验计划 1, 如下的物料号, QM视图里有激活01检验类型,同时勾选了Multiple Specs选项 ...

最新文章

  1. 【转载】C#扫盲之:==/Equals /ReferenceEquals 异同的总结,相等性你真的知道吗?
  2. 【 FPGA/IC 】谈谈复位
  3. c从sqlite3数据库中获取数据,并对数据进行拼接
  4. vba 自动排序_学会这个Excel表格技巧之后,立刻实现自动排序,太牛了
  5. 并发编程——进程池与线程池
  6. LeetCode 1122 数组的相对排序-简单-unordered_map容器的应用
  7. 8.21 :odd??:nth-of-type??
  8. [CF718C] Sasha and Array
  9. 作为相亲大户,程序猿为何普遍单身?
  10. 防淘宝关闭二维码案例
  11. 安装Labview2012 “labview 2012 未定义必须的 NIPathsDir属性 maxAFWDIR”
  12. HSRP+生成树+vlan间路由!
  13. RXJAVA之Subject
  14. ug中文字大小设置_UG编辑文字怎么放大或缩小?
  15. SVN学习:SVN的下载安装
  16. 记录一直以来看过的电视剧、电影及书籍
  17. 根因定位FluxRank论文背景说明
  18. ERP系统对接淘宝电商和线下工作人员的问题与解决方案
  19. 人工智能知识体系梳理
  20. 论Cardano修仙之路,聊ADA现状分析

热门文章

  1. 学习pytorch的一些自己犯过的错误而总结的注意事项,估计其他也会使用
  2. gitlab 删除分支_idea gitlab 分支 pull、push 实践笔记
  3. python发送图片邮件exchangelib_python基于exchange函数发送邮件过程详解
  4. Ajax请求中async属性
  5. css让image不改变大小_如何改变图片大小
  6. 晓庄2019c语言真题卷,南京晓庄学院—C语言期末考试复习提纲
  7. python装饰器 廖雪峰_python装饰器的一个妙用
  8. linux 内核dump,linux内核调试技巧之一 dump_stack【转】
  9. python后端程序例子_Python MR程序示例
  10. PowerDesigner16.5汉化破解版安装教程(含安装文件、汉化包、破解文件)