概述
首先我们先来看看账户类
class Account {
// 余额
private TxnRef<Integer> balance;
// 构造方法
public Account(int balance) {
this.balance = new TxnRef<Integer>(balance);
}
// 转账操作,该操作我们要保证 1.txn这个事务是原子性
2.每个账户对象的balance是一致性的(就是转账,两者总数不变)
public void transfer(Account target, int amt) {
STM.atomic((txn) -> {
Integer from = balance.getValue(txn);
balance.setValue(from - amt, txn);
Integer to = target.balance.getValue(txn);
target.balance.setValue(to + amt, txn);
});
}
}
下面我们介绍上面代码中余额TxnRef这个引用对象。
/*
* 事务的引用。(不是事务,你可以想象成
数据库中的一条数据引用)
* 该方法中存在模仿MVCC的并发版本控制的余额版本对象VersionedRef
*
以及在当前事务中读取数据和写数据两个方法,当然这两个方法交给其他人Txn来做
*/
public class TxnRef<T> {
// 当前数据,带版本的数据(当前事务中,保存的最新的数据)
volatile VersionedRef curRef;
public TxnRef(T value) {
this.curRef = new VersionedRef(value, 0L);
}
// 获取当前事务的数据
public T getValue(Txn txn) {
// 事务的读操作交给Txn接口
return txn.get(this);
}
// 在当前事务中设置数据
public void setValue(T value, Txn txn) {
// 事务的写操作交给Txn接口
txn.set(this, value);
}
}
根据上面TxnRef类先来展示下VersionedRef 真正的带版本数据,然后我们在来看实现读写数据引用TxnRef的Txn类。
/*
* 该类是带版本的数据对象
* 数据和版本是一一对应的,没有修改数据,版本号不变,修改数据,版本号+1
*/
public final class VersionedRef<T> {
final T value;
final long version;
public VersionedRef(T value, long version) {
this.value = value;
this.version = version;
}
}
修改数据,让版本号+1操作是交给实现了Txn接口的STMTxn类,让我们来看看这个实现了读写数据引用TxnRef的Txn实现类。
该实现类的三个方法:
1. get方法,把要读的对象都添加到inTxnMap 中。
2. set方法,把要修改的对象,先读添加到inTxnMap 中,把修改后的对象保存在writeMap中
3. commit方法,为了简单实现,使用互斥锁的方式,事务的提交变成串行了,首先先检查inTxnMap 中数据是否发生变化,如果没有,就直接将writeMap中的数据写入(就是把账户对象的余额修改),如果发生变化,break结束,当然我们不能因为余额发生变化就不转账了,下面就会介绍到STM类,重新创建一个事务STMTxn 再提交,知道提交成功。
//接口
public interface Txn {
<T> T get(TxnRef<T> ref);
<T> void set(TxnRef<T> ref, T value);
}
/*实现类
* 事务中的值得读写是交给这个实现类的。
*/
public class STMTxn implements Txn {
// 事务id生成器(唯一)
private static AtomicLong txnSeq = new AtomicLong(1);
// 当前事务所有相关的数据
private Map<TxnRef, VersionedRef> inTxnMap = new HashMap();
// 当前事务所有需要修改的数据
private Map<TxnRef, Object> writeMap = new HashMap();
// 当前事务id
private long txnId;
// 自动生成当前事务id
public STMTxn() {
this.txnId = txnSeq.getAndIncrement();
}
@Override
// 获取当前事务中的数据
public <T> T get(TxnRef<T> ref) {
// 将需要读取的数据加入到inTxnMap中,同时保证一个事务中读取的事务是同一个版本
if (!inTxnMap.containsKey(ref)) {
inTxnMap.put(ref, ref.curRef);
}
return (T) inTxnMap.get(ref).value;
}
@Override
public <T> void set(TxnRef<T> ref, T value) {
// 将需要修改的数据加入到inTxnMap中
if (!inTxnMap.containsKey(ref)) {
inTxnMap.put(ref, ref.curRef);
}
// 将要修改的Object放入writeMap
writeMap.put(ref, value);
}
// 提交事务
public boolean commit() {
synchronized (STM.commitLock) {
// 是否校验通过
boolean isValid = true;
// 校验所有读过的数据是否发生过变化
for (Map.Entry entry : inTxnMap.entrySet()) {
VersionedRef curRef = ((TxnRef) entry.getKey()).curRef;
// 该事务读数据的时候的值
VersionedRef readRef = (VersionedRef) entry.getValue();
// 通过版本号来验证数据是否发生过变化
if (curRef.version != readRef.version) {
isValid = false;
break;
}
}
// 如果校验通过,则所有更改生效
if (isValid) {
writeMap.forEach((k, v) -> {
//这里把事务的原子类id,作为版本
k.curRef = new VersionedRef(v, txnId);
});
}
return isValid;
}
}
}
当然我们需要提供一个实现事务操作的入口。就比如上面Account类转账一系列操作如何变成事务?
请看代码
//函数式接口
@FunctionalInterface
public interface TxnRunnable {
void run(Txn txn);
}
public final class STM {
// 私有化构造方法
private STM() {
}
// 提交数据需要用到的全局锁
static final Object commitLock = new Object();
// 原子化提交方法,这里引入函数式接口
public static void atomic(TxnRunnable action) {
boolean committed = false;
// 如果没有提交成功,则一直重试
while (!committed) {
// 创建新的事务
STMTxn txn = new STMTxn();
// 执行业务逻辑
action.run(txn);
// 提交事务
committed = txn.commit();
}
}
}
当Account类执行转账操作,调用STM工具类atomic方法,开启事务,while循环执行事务,直到成功执行,执行事务中,调用函数式接口执行run方法,传入txn事务,执行事务get,set方法.
事务1:CountA转账CountB100元,先执行写操作不提交。等事务2:CountB转账CountC100元,提交事务之后,再提交事务1.
这个过程无法直观展示,所以你只能结合下面的代码+想象喽。
public static void main(String[] args) {
Account account1 = new Account(500);
Account account2 = new Account(500);
Account account3 = new Account(500);
new Thread(() -> {
account1.transfer(account2, 100);
}).start();
new Thread(() -> {
account2.transfer(account3, 100);
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(account1.balance.curRef.value);
System.out.println(account2.balance.curRef.value);
System.out.println(account3.balance.curRef.value);
}
}
最后
以上就是风趣唇膏为你收集整理的java使用STM原理实现转账的全部内容,希望文章能够帮你解决java使用STM原理实现转账所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复