在一些函数式编程语言中,支持一种叫做软件事务内存(STM)的技术。这里的事务和数据库中所说的事务非常类似,具有隔离性、原子性和一致性。与数据库事务不同的是,内存事务不具备持久性。
在很多场合,某一项功能可能要由多个Actor协作完成。在这种协作事务中,如果一个Actor处理失败,那么根据事务的原子性,其他Actor所进行的操作必须要回滚。下面,看一个简单的案例。
假设有一个公司要给它的员工发福利,公司账户里有100元。每次,公司账户会给员工账户转一笔钱,假设转账10元,那么公司账户中应该减去10元,同时,员工账户中应该增加10元。这两个操作必须同时完成,或者同时不完成。
首先,看一下主函数中是如何启动一个内存事务的:
public class STMDemo {
public static ActorRef company = null;
public static ActorRef employee = null;
public static void main(String[] args) throws Exception {
final ActorSystem system = ActorSystem.create("transactionDemo", ConfigFactory.load("samplehello.conf"));
company = system.actorOf(Props.create(CompanyActor.class), "company");
employee = system.actorOf(Props.create(EmployeeActor.class), "employee");
Timeout timeout = new Timeout(1, TimeUnit.SECONDS);
for(int i=1; i<20; i++) {
company.tell(new Coordinated(i,timeout), ActorRef.noSender());
Thread.sleep(200);
Integer companyCount = (Integer) Await.result(Patterns.ask(company, "GetCount", timeout), timeout.duration());
Integer employeeCount = (Integer) Await.result(Patterns.ask(employee, "GetCount", timeout), timeout.duration());
System.out.println("company count=" + companyCount);
System.out.println("employee count=" + employeeCount);
System.out.println("===============");
}
}
}
上述代码中,CompanyActor和EmployeeActor分别用于管理公司账户和雇员账户。在12~23行中,尝试进行19次汇款,第一次汇款额度为1元,第二次为2元,依次类推,最后一笔汇款为19元。
在第13行,新建一个Coordinated协调者,并且将这个协调者当做消息发送给company,当company收到这个协调者信息后,自动成为这个事务的第一个成员。
第15~18行询问公司账户和雇员账户的当前余额,并在第20~21行进行输出。
下面是代表公司账户的Actor:
public class CompanyActor extends UntypedActor {
private Ref.View<Integer> count = STM.newRef(100);
@Override
public void onReceive(Object msg) throws Exception {
if(msg instanceof Coordinated) {
final Coordinated c = (Coordinated)msg;
final int downCount = (Integer)c.getMessage();
STMDemo.employee.tell(c.coordinate(downCount), getSelf());
try {
c.atomic(new Runnable() {
@Override
public void run() {
if(count.get() < downCount) {
throw new RuntimeException("less than " + downCount);
}
STM.increment(count, -downCount);
}
});
} catch (Exception e) {
e.printStackTrace();
}
} else if("GetCount".equals(msg)) {
getSender().tell(count.get(), getSelf());
} else {
unhandled(msg);
}
}
}
在CompanyActor中,首先判断接收的msg是否是Coordinated。如果是Coordinated,则表示这是一个新事务的开始。在第8行,获得事务的参数也就是需要转账的金额。接着在第9行,将调用Coordinated.coordinate()方法,将employee也加入当前事务中,这样这个事务中就有两个参与者。
第11行,调用了Coordinated.atomic()定义了原子执行块作为这个事务的一部分。在这个执行块中,对公司账户进行余额调整(第17行)。但是当汇款额度大于可用额度时,就会抛出异常,宣告失败。
第25行用于处理GetCount消息,返回当前账户余额。
作为转账接收的雇员账户如下:
public class EmployeeActor extends UntypedActor {
private Ref.View<Integer> count = STM.newRef(50);
@Override
public void onReceive(Object msg) throws Exception {
if(msg instanceof Coordinated) {
final Coordinated c = (Coordinated)msg;
final int downCount = (Integer)c.getMessage();
try {
c.atomic(new Runnable() {
@Override
public void run() {
STM.increment(count, downCount);
}
});
} catch (Exception e) {
// TODO: handle exception
}
} else if("GetCount".equals(msg)) {
getSender().tell(count.get(), getSelf());
} else {
unhandled(msg);
}
}
}
上述代码第2行,设置雇员账户初始金额是50元。第6行,判断消息是否为Coordinated,如果是Coordinated,则当前Actor会自动加入Coordinated指定的事务。第10行,定义原子操作,在这个操作中将修改雇员账户余额。在这里,并没有给出异常情况的判断,只要接收到转入金额,一律将其增加到雇员账户中。
在这里,两个Actor都已经加入到同一个协调事务Coordinated中,因此当公司账户出现异常后,雇员账户就会回滚。
执行上述程序,部分输出如下:
company count=55
employee count=95
===============
company count=45
employee count=105
===============
company count=34
employee count=116
===============
company count=22
employee count=128
===============
company count=9
employee count=141
===============
java.lang.RuntimeException: less than 14
. . . .
company count=9
employee count=141
===============
可以看到,无论转账操作是否成功,公司账户和雇员账户的金额总是一致的。当转账失败时,雇员账户的余额并不会增加。这就是软件事务内存的作用。