软件事务内存导论(十)处理写偏斜异常

处理写偏斜异常

在6.6节中,我们曾经简单讨论了写偏斜(write skew)以及Clojure
STM是如何解决这个问题的。Akka同样提供了处理写偏斜问题的支持,但是需要我们配置一下才能生效。OK,一听到配置这个词可能让你觉得有些提心吊
胆,但实际操作起来其实起来还是蛮简单的。下面就让我们首先了解一下Akka在不进行任何配置情况下的默认行为。

让我们回顾一下之前曾经见到过的那个多个账户共享同一个联合余额最低限制例子。首先我们创建了一个名为Portfolio的类来保存支票账户余额和
储蓄账户余额。根据银行规定,这两个账户的总余额不得低于$1000。在Portfolio类的代码中我们用Java重新实现了withdraw()函
数。在该函数中,我们先读取两个账户的余额,将二者相加得到总余额,并在等待一个故意插进去的延时(引入这个延时的目的是为了人为制造事务冲突的环境)之
后,从其中一个账户余额中减掉给定数量的金额(当然,在操作之前需要判断减掉这个数量后总余额不少于$1000)。最后需要注意的
是,withdraw()函数是在一个使用了默认设置的事务中完成上述操作的。

01 public  class  Portfolio  {
02     final  private  Ref<Integer>  checkingBalance  =  new  Ref<Integer>(500);
03     final  private  Ref<Integer>  savingsBalance  =  new  Ref<Integer>(600);
04     public  int  getCheckingBalance()  {  return  checkingBalance.get();  }
05     public  int  getSavingsBalance()  {  return  savingsBalance.get();  }
06  
07     public  void  withdraw(final  boolean  fromChecking,  final  int  amount)  {
08         new  Atomic<Object>()  {
09             public  Object  atomically()  {
10                 final  int  totalBalance  =
11                     checkingBalance.get()  +  savingsBalance.get();
12                 try  {  Thread.sleep(1000);  }  catch(InterruptedException  ex)  {}
13                 if(totalBalance  -  amount  >=  1000)  {
14                     if(fromChecking)
15                         checkingBalance.swap(checkingBalance.get()  -  amount);
16                     else
17                         savingsBalance.swap(savingsBalance.get()  -  amount);
18                 }
19                 else
20                     System.out.println(
21                         "Sorry,  can't  withdraw  due  to  constraint  violation");
22                     return  null;
23                 }
24         }.execute();
25     }
26 }

下面让我们创建两个事务来并发地更改账户内的余额:

01 public  class  UsePortfolio  {
02     public  static  void  main(final  String[]  args)  throws  InterruptedException  {
03         final  Portfolio  portfolio  =  new  Portfolio();
04         int  checkingBalance  =  portfolio.getCheckingBalance();
05         int  savingBalance  =  portfolio.getSavingsBalance();
06         System.out.println("Checking  balance  is  "  +  checkingBalance);
07         System.out.println("Savings  balance  is  "  +  savingBalance);
08         System.out.println("Total  balance  is  "  +
09             (checkingBalance  +  savingBalance));
10         final  ExecutorService  service  =  Executors.newFixedThreadPool(10);
11         service.execute(new  Runnable()  {
12             public  void  run()  {  portfolio.withdraw(true100);  }
13         });
14         service.execute(new  Runnable()  {
15             public  void  run()  {  portfolio.withdraw(false100);  }
16         });
17         service.shutdown();
18         Thread.sleep(4000);
19         checkingBalance  =  portfolio.getCheckingBalance();
20         savingBalance  =  portfolio.getSavingsBalance();
21         System.out.println("Checking  balance  is  "  +  checkingBalance);
22         System.out.println("Savings  balance  is  "  +  savingBalance);
23         System.out.println("Total  balance  is  "  +
24         (checkingBalance  +  savingBalance));
25         if(checkingBalance  +  savingBalance  <  1000)
26         System.out.println("Oops,  broke  the  constraint!");
27     }
28 }

正如我们在输出结果中所看到的那样,在默认情况下,Akka没能避免写偏斜问题,两个事务违反了银行的规定,即都从账户里取出了钱。

Checking  balance  is  500
Savings  balance  is  600
Total  balance  is  1100
Checking  balance  is  400
Savings  balance  is  500
Total  balance  is  900
Oops,  broke  the  constraint!

现在到了该彻底解决这个问题的时候了。让我们祭出TransactionFactory这个能帮助我们在程序里对事物进行配置的法宝,在Portfolio类的第9行插入下面这段创建工厂实例的代码:

1 akka.stm.TransactionFactory  factory  =
2     new  akka.stm.TransactionFactoryBuilder()
3         .setWriteSkew(false)
4         .setTrackReads(true)
5         .build();

在插进来的这几行代码中,我们创建了一个TransactionFactoryBuilder,并将writeSkew和trackReads属性
分别设置为false和true。与Clojure
STM对于ensure的处理类似,这两个设置项的目的是告诉事务要在其运行过程中对读操作进行追踪,同时也会使事务在读数据的过程中对账户余额变量加读
锁直至提交开始为止。

除了上面提到的几处更改之外,Portfolio和UsePortfolio的其他代码都保持不变。而在对事务进行了上述设置之后,其输出结果如下所示:

Checking  balance  is  500
Savings  balance  is  600
Total  balance  is  1100
Sorry,  can't  withdraw  due  to  constraint  violation
Checking  balance  is  400
Savings  balance  is  600
Total  balance  is  1000

由于并发执行的不可预测性,我们不能确定两个事务到底哪个会胜出。但是我们可以从输出结果中看到,在所有操作结束后两个账户的余额是不同的,而在6.6节的Clojure示例中,最终两个账户余额是相同的。我们可以通过多次运行这两个实例来观察二者之间的差异。

在本节我们是用Java完成整个示例的。如果换成Scala,则我们可以使用在6.10节中学习的语法来配置事务的writeSkew和trackReads属性。

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

时间: 2024-11-13 12:10:08

软件事务内存导论(十)处理写偏斜异常的相关文章

软件事务内存导论(二)软件事务内存

1.1    软件事务内存 将实体与状态分离的做法有助于STM(软件事务内存)解决与同步相关的两大主要问题:跨越内存栅栏和避免竞争条件.让我们先来看一下在Clojure上下文中的STM是什么样子,然后再在Java里面使用它. 通过将对内存的访问封装在事务(transactions)中,Clojure消除了内存同步过程中我们易犯的那些错误(见 <Programming Clojure>[Hal09]和<The Joy of Clojure>[FH11]).Clojure会敏锐地观察和

软件事务内存导论

前言 软件事务内存 用Akka/Multiverse STM实现并发 创建事务 创建嵌套事务 配置Akka事务 阻塞事务 提交和回滚事件 集合与事务 处理写偏斜异常 STM的局限性 文章转自 并发编程网-ifeve.com

软件事务内存导论(八)提交和回滚事件

提交和回滚事件 Java的try-catch-finally语法结构不但使我们可以安全地处理异常,还能够在程序抛出异常时选择性地执行一些代码.同样地,我们也可以控制程序在事务成功提交之后去执行某段代码,而当事务回滚时则去执行另一段代码.StmUtils中的deferred()和compensatiing()这两个函数分别提供了上述功能.特别地,在实现事务的过程中,为保证事务能顺利完成,我们通常会加入一些带副作用的逻辑,而deferred()函数则是一个执行所有这部分逻辑的绝佳地点. Java中的

软件事务内存导论(三)用Akka/Multiverse STM实现并发

用Akka/Multiverse STM实现并发 上面我们已经学习了如何在Clojure里使用STM,我猜你现在一定很好奇如何在Java代码中使用STM.而对于这一需求,我们有如下选择: 直接在Java中使用Clojure STM.方法非常简单,我们只需将事务的代码封装在一个Callable接口的实现中就行了,详情请参见第7章. 喜欢用注解(annotation)的开发者可能会更倾向于使用Multiverse的STM API. 除了STM之外,如果我们计划使用角色(actor),那么还可以考虑选

软件事务内存导论(五)创建嵌套事务

1.1    创建嵌套事务 在之前的示例中,每个用到事务的方法都是各自在其内部单独创建事务,并且事务所涉及的变动也都是各自独立提交的.但如果我们想要将多个方法里的事务调整成一个统一的原子操作的时候,上述做法就无能为力了,所以我们需要使用嵌套事务来实现这一目标. 通过使用嵌套事务,所有被主控函数调用的那些函数所创建的事务都会默认被整合到主控函数的事务中.除此之外,Akka/Multiverse还提供 了很多其他配置选项,如新隔离事务(new isolated transactions)等.总之,使

软件事务内存导论(十一)-STM的局限性

1.1    STM的局限性 STM消除了显式的同步操作,所以我们在写代码时就无需担心自己是否忘了进行同步或是否在错误的层级上进行了同步.然而STM本身也存在一些问题,比如在跨越内存栅栏失败或遭遇竞争条件时我们捕获不到任何有用的信息.我似乎可以听到你内心深处那个精明的程序员在抱怨"怎么会这样啊?".确实,STM是有其局限性的,否则本书写到这里就应该结束了.STM只适用于写冲突非常少的应用场景,如果你的应用程序存在很多写操作竞争,那么我们就需要在STM之外寻找解决方案了. 下面让我们进一

软件事务内存导论(九) 集合与事务

集合与事务 在我们努力学习这些示例的过程中,很容易就会忘记我们所要处理的值都必须是不可变的.只有实体才是可变的,而状态值则是不可变的.虽然STM已经为我们减轻了很多负担,但如果想要在维护不可变性的同时还要兼顾性能的话,对我们来说也将是一个非常严峻的挑战. 为了保证不可变性,我们采取的第一个步骤是将单纯用来保存数据的类(value classes)及其内部所有成员字段都置为final(在Scala中是val).然后,我们需要传递地保证我们自己定义的类里面的字段所使用的类也都 是不可变的.可以说,将

软件事务内存导论(六)配置Akka事务

配置Akka事务 默认情况下,Akka为其相关的运行参数都设定了默认值,我们可以通过代码或配置文件akka.conf来更改这些默认设置.如果想了解如何指定或修改该配置文件位置的详细信息,请参阅Akka的文档. 针对单个事务,我们可以利用TransactionFactory在程序代码中更改其设置.下面就让我们用这种方式先后在Java和Scala中更改一些设置来为你展示如何实现设置的变更. 在Java中对事务进行配置 01 public  class  CoffeePot  { 02     pri

软件事务内存导论(四)创建事务

创建事务 我们创建事务的目的是为了协调针对多个托管引用的变更.事务将会保证这些变更是原子的,也就是说,所有的托管引用要么全部被提交要么全部被丢弃,所以在事务之外我们将不会看到有任何局部变更(partial changes)出现.此外,我们也可以用创建事务的方式来解决对单个ref先读后写所引发的相关问题. Akka是用Scala开发出来的,所以如果我们工作中用的是Scala的话,就可以直接幸福地享用Akka简洁明了的API了.对于那些日常工作中不能使用Scala开发的程序员,Akka同样也提供了一