两种情况分析:1、@Transactional+Synchronized。2、AOP+锁。扩展:分布式锁
point:文章中的内容本人并没有在实际中用过也没有测试过,只是想到了这个问题所以按自己目前的理解做一个记录,正确性待验证。
@Transactional+Synchronized
在使用Spring的@Transactional处理事务时虽然有事务隔离机制,但是在SERIALIZABLE以下的隔离级别并不能解决并发情况下的幻读、脏读问题。看下面的一个例子:
@Transactional
public void update() {
boolean index = select(....);
if(index) {
update( ...index = false...,);
doSomthing()
}
}
@Transactional默认的隔离级别是可重复读,按道理说是没有脏读、不可重复读等问题的,但是细想一下,假如以上代码是为了保证doSomthing()只被执行一次。如果两个线程对应的事务A和事务B都进行了select操作(因为该隔离级别下读不阻塞),返回都为true。然后事务A进行了update并执行了doSomthing()。当A执行完updat,B又会接着往下执行,会再执行一次doSomthing()。
所以就需要业务层来实现严格的同步,Java中锁实现有Synchronized和ReentrantLock两种,代码可能是这个样子的:
private ReentrantLock lock = new ReentrantLock();
@Transactional
public void update() {
lock.lock();
try {
boolean index = select(....);
if(index) {
update( ...index = false...,);
doSomthing()
}
}finally {
lock.unlock();
}
}
------------------------
@Transactional
public synchronized void update() {
boolean index = select(....);
if(index) {
update( ...index = false...,);
doSomthing()
}
}
不管是Synchronized还是ReentrantLock,其实这样写都还是不能保证同步,因为Spring的@Transactional是AOP实现的,它是在函数开始前开启事务,在函数执行完毕后提交事务。所以上面代码中锁的释放是在事务提交之前,所以还是会有前面提到的问题。
AOP+锁
上面提到的锁无效是因为锁的范围是在AOP事务范围之内,那针对这个的解决方案可以使用AOP+锁实现,我们可以自定义一个锁注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLock {
}
然后定义一个AOP切面:
@Component
@Aspect
public class LockAspect implements Ordered {
private static ReentrantLock lock = new ReentrantLock();
@Pointcut("@annotation(MyLock)")
public void dolock(){}
@Around("dolock()")
public void testLock(ProceedingJoinPoint joinPoint) {
lock.lock();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
...
}finally {
lock.unlock();
}
}
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; //小的先执行
}
}
然后我们就可以这样实现锁同步:
@MyLock
@Transactional
public synchronized void update() {
boolean index = select(....);
if(index) {
update( ...index = false...,);
doSomthing()
}
}
分布式锁
上面的锁实现在分布式环境下都是无效的,因为分布式下lock不是一个对象。我了解的解决方案有下面几个:
1、自己通过数据库实现锁
既然reentranLock和Synchronized不能使用的原因是锁对象不是同一个,那是不是可以通过数据库来实现同一个锁对象?在数据库中定义一个锁表,插入一个状态记录(比如0/1、true/false),当线程读取数据库记录是0的时候就视为获取锁,并将状态修改为1。当业务操作执行完要释放锁的时候就将状态改回0。或者直接利用数据库的唯一性约束,设置唯一主键,每次获取锁的操作都是一个insert操作,因为唯一性约束所以只会有一个线程insert成功,即获得锁,释放锁时delete该条记录就行。
this.SQL_TRY_ACQUIRE_LOCK = "update " + props.getTableName() + " set locked = true ,token = ? ,start_time = ? ,expire_time = ? ,end_time = null
where id = ? and (locked = false or expire_time < ?) ";
this.SQL_SELECT_LOCK = "select id ,name ,locked ,token ,start_time ,end_time ,expire_time ,attributes from " + props.getTableName() + "
where id = ? ";
public DLock acquireLock(DLockType type, String lockToken, long currTime, long expireTime) {
this.template.update(this.SQL_TRY_ACQUIRE_LOCK, new Object[]{lockToken, new Timestamp(currTime), new Timestamp(expireTime), type.getId(), new Timestamp(currTime)});
List<DLock> locks = this.template.query(this.SQL_SELECT_LOCK, new Object[]{type.getId()}, this.rowMapper);
if (locks != null && !locks.isEmpty()) {
DLock lock = (DLock)locks.get(0);
if (lockToken.equals(lock.getLockToken())) {
return lock;
} else {
long sysCurrTime = System.currentTimeMillis();
if (!lock.isLocked() || lock.getExpireTime() != null && lock.getExpireTime() >= currTime) {
return null;
} else {
logger.debug("LockExpireTime={}, CurrTime={}, SysCurrTime={}", new Object[]{lock.getExpireTime(), currTime, sysCurrTime});
return lock;
}
}
} else {
return null;
}
}
以上代码先update,如果锁空闲就可以update成功,并且update的时候数据库锁会保证其他线程不能操作该条数据。
2、利用缓存如redis实现
3、利用zookeeper实现
网友评论