java使用STM原理实现转账

首先我们先来看看账户类

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);

	}
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值