最近在修改Seata
线程并发的一些问题,把其中一些经验总结给你们。先简单描述一下这个问题,在Seata
这个分布式事务框架中有个全局事务的概念,在大多数状况下,全局事务的流程基本是顺序推动不会出现并发问题,可是当一些极端的状况下,会出现多线程访问致使咱们全局事务处理不正确。 以下面代码所示: 在咱们全局事务commit
阶段,有一个以下代码:java
if (status == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Committing);
}
复制代码
代码有些省略,就是先判断status状态是否Begin状态,而后改变状态为Committing。git
在咱们全局事务rollback阶段,有一个以下代码:github
if (status == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
}
复制代码
一样的也省略了部分代码,这里先判断status
状态是否为begin
,而后改变为Rollbacking
。这里再Seata
的代码中并无作一些线程同步的手段,若是这两个逻辑同时执行(通常状况下不会,可是极端状况下可能会出现),会让咱们的结果出现不可预料的错误。而咱们所要作的就是解决这种极端状况下来的并发出现的问题。数据库
对于这种并发出现问题我相信你们第一时间想到的确定是加锁,在Java中咱们咱们通常采用下面两个手段进行加锁:编程
Synchronized
ReentrantLock
咱们能够利用Synchronized
或者 ReentrantLock
进行加锁,能够将代码修改为下面的逻辑:安全
synchronized:bash
synchronized(globalSession){
if (status == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
}
}
复制代码
ReentrantLock
进行加锁:多线程
reentrantLock.lock();
try {
if (status == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
}
}finally {
reentrantLock.unlock();
}
复制代码
对于这种加锁比较简单,在Seata
的Go-Server
中目前是这样实现的。可是这种实现场景忽略了咱们上面所说的一种状况,就是极端状况下,也就是有可能99.9%的状况下可能不会出现并发问题,只有%0.1的状况可能致使这个并发问题。虽然咱们悲观锁一次加锁的时间也比较短,可是在这种高性能的中间件中仍是不够,那么就引入了咱们的乐观锁。并发
一提起乐观锁,不少朋友都会想到数据库中乐观锁,想象一下上面的逻辑若是在数据库中,而且没有利用乐观锁去作,咱们会有以下的伪代码逻辑:框架
select * from table where id = xxx for update;
if(status == begin){
//do other thing
update table set status = rollbacking;
}
复制代码
上述代码在咱们不少的业务逻辑中都能看见,这段代码有两个小问题:
因此为了解决上面的问题,在不少若是竞争不大的场景下,咱们就采用了乐观锁的方法,咱们在数据库中加一个字段version表明着版本号,咱们将代码修改为以下所示:
select * from table where id = xxx ;
if(status == begin){
//do other thing
int result = (update table set status = rollbacking where version = xxx);
if(result == 0){
throw new someException();
}
}
复制代码
这里咱们的查询语句再也不有for update,咱们的事务也只缩小到update一句,咱们经过咱们第一句查询出来的version来进行判断,若是咱们的更新的更新的行数为0,那么就证实其余事务对他进行了修改。这里能够抛出异常或者作一些其余的事。
从这里能够看出咱们使用乐观锁将事务较大,锁定较长这两个问题都解决,可是对应而来的成本就是若是更新失败咱们可能就会抛出异常或者作一些其余补救的措施,而咱们的悲观锁在执行业务以前都已经限制住了。因此咱们这里使用乐观锁必定只能在对某条数据并发处理的状况比较小的状况下。
咱们上面讲述了在数据库中的乐观锁,不少人就在问,没有数据库,在咱们代码中怎么去实现乐观锁呢?熟悉synchronized
的同窗确定知道synchronized
在Jdk1.6以后对其进行了优化,引入了锁膨胀的一个模型:
上面的级种锁模型中轻量级锁所适用的线程交替进入临界区很适合咱们的场景,由于咱们的全局事务通常来讲不会是某个单线程一直在处理该事务(固然也能够优化成这个模型,只是设计会比较复杂),咱们的全局事务再大多数状况下都会是不一样线程交替进入处理这个事务逻辑,因此咱们能够借鉴轻量级锁CAS自旋的思想,完成咱们代码级别的自旋锁。这里也有朋友可能会问为何不用synchronized呢?这里通过实测在交替进入临界区咱们本身实现的CAS自旋性能是最高的,而且synchronized没有超时机制,不方便咱们处理异常状况。
class GlobalSessionSpinLock {
private AtomicBoolean globalSessionSpinLock = new AtomicBoolean(true);
public void lock() throws TransactionException {
boolean flag;
do {
flag = this.globalSessionSpinLock.compareAndSet(true, false);
}
while (!flag);
}
public void unlock() {
this.globalSessionSpinLock.compareAndSet(false, true);
}
}
// method rollback
void rollback(){
globalSessionSpinLock.lock();
try {
if (status == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
}
}finally {
globalSessionSpinLock.unlock();
}
}
复制代码
上面咱们用CAS
简单的实现了一个乐观锁,可是这个乐观锁有个小缺点就是一旦出现竞争不能膨胀为悲观锁阻塞等待,而且也没有过时超时,有可能大量占用咱们的CPU
,咱们又继续进一步优化:
public void lock() throws TransactionException {
boolean flag;
int times = 1;
long beginTime = System.currentTimeMillis();
long restTime = GLOBAL_SESSOION_LOCK_TIME_OUT_MILLS ;
do {
restTime -= (System.currentTimeMillis() - beginTime);
if (restTime <= 0){
throw new TransactionException(TransactionExceptionCode.FailedLockGlobalTranscation);
}
// Pause every PARK_TIMES_BASE times,yield the CPU
if (times % PARK_TIMES_BASE == 0){
// Exponential Backoff
long backOffTime = PARK_TIMES_BASE_NANOS << (times/PARK_TIMES_BASE);
long parkTime = backOffTime < restTime ? backOffTime : restTime;
LockSupport.parkNanos(parkTime);
}
flag = this.globalSessionSpinLock.compareAndSet(true, false);
times++;
}
while (!flag);
}
复制代码
上面的代码作了以下几个优化:
parkTime
时间,挂起以后又继续循环获取,若是再次获取不到,此时咱们会对咱们的parkTime进行指数退避形式的挂起,将咱们的挂起时间逐渐增加,直到超时。从咱们对并发控制的处理来看,想要达到一个目的,要实现它方法是有多种多样的,咱们须要根据不一样的场景,不一样的条件,选择合适的方法,选择最高效的手段来完成咱们的目的。本文没有对悲观锁的原理作太多的阐述,这里有兴趣的能够下来自行查阅资料,读完本文若是你只能记住一件事,那么请记住实现线程并发安全的时候别忘记考虑乐观锁。
最后这篇文章被我收录于JGrowing
并发编程篇,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:github.com/javagrowing… 麻烦给个小星星哟。
若是你们以为这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O: