在web项目开发过程中,经常会遇到分布式资源控制的场景,通过加锁从而保证资源访问的互斥性。本文主要介绍在没有redis情况下通过mysql进行分布式锁的实现。
场景:
线程A与线程B执行前需要判断资源R的状态,当R的状态为1时,则可以执行,当R的状态为0时,则不容许执行,且同一时刻只容许一个线程执行。线程执行时资源R状态置0,线程执行结束后R状态重新置1.
上述场景中,若不对资源R进行互斥访问,则可能出现A、B线程同时访问资源R时且发现资源状态为1,从而都启动执行无法满足系统要求。
当资源R状态的获取和资源状态R的置位需要操作mysql进行实现时,我们则需要将查找和更新操作作为原子性操作,这种情况下,我们会考虑到事务,事务有如下几种隔离级别:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
而通常在web项目配置过程中,事务的隔离级别为READ_COMMITTED(2),可以避免脏读,依旧无法避免不可重复读。因此不可避免的需要将查找和更新操作进行加锁,同一时刻只允许一个线程访问操作。
AOP 实现
1.定义DB锁接口
public interface DBLock {
/**
* 加锁
*
* @param timeout 超时时间
* @param expireTime 过期时间
* @param timeUnit 时间单位
* @return 是否加锁成功
*/
default boolean lock(long timeout, long expireTime, TimeUnit timeUnit) {
throw new ServerException("unimplemented function");
}
/**
* 释放锁
*/
default void unlock() {
throw new ServerException("unimplemented function");
}
}
2.可重复锁实现
@Slf4j
public class DBReentrantLock implements DBLock {
/**
* 锁资源
*/
private LockService lockService;
/**
* 锁路径
*/
private String lockPath;
/**
* 可重入数量
*/
@Getter
private int count = 0;
/**
* 过期时间
*/
@Getter
private long expireTime = 0;
DBReentrantLock(LockService lockService, String lockPath) {
this.lockService = lockService;
this.lockPath = lockPath;
this.count = 0;
}
/**
* 获取锁
*
* @param timeout 超时时间
* @param expireTime 过期时间
* @param timeUnit 时间单位
* @return 是否获取到锁
*/
@Override
public boolean lock(long timeout, long expireTime, TimeUnit timeUnit) {
try {
// 已获得锁
if (this.count > 0) {
this.count++;
return true;
}
if (timeout < 0)