在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),可以避免脏读,依旧无法避免不可重复读。因此不可避免的需要将查找和更新操作进行加锁,同一时刻只允许一个线程访问操作。
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) {
return false;
}
lockService.clearExpired();
if (lockService.lock(lockPath, timeUnit.toMillis(expireTime))) {
this.count = 1;
this.expireTime = System.currentTimeMillis() + timeUnit.toMillis(expireTime);
return true;
}
// 获取锁是不等待
if (timeout == 0) {
return false;
}
long threshold = timeUnit.toMillis(timeout) + System.currentTimeMillis();
while (System.currentTimeMillis() < threshold) {
// 清除过期锁
lockService.clearExpired();
if (lockService.lock(lockPath, timeUnit.toMillis(timeout))) {
this.count = 1;
this.expireTime = System.currentTimeMillis() + timeUnit.toMillis(expireTime);
return true;
}
try {
Thread.sleep(200);
} catch (Exception ignored) {};
}
return false;
} catch (Exception e) {
log.error("can not acquire {} lock, unknown error happened", lockPath, e);
throw new ServerException("this should not happened", e);
}
}
/**
* 释放锁
*/
@Override
public void unlock() {
try {
this.count--;
if (this.count == 0) {
this.expireTime = 0;
this.lockService.release(lockPath);
}
log.debug("release {} lock successfully", lockPath);
} catch (Exception e) {
log.error("can not release {} lock, unknown error happened", lockPath, e);
}
}
}
3.锁上下文实现
@Slf4j
@Component
public class LockContext {
@Autowired
private LockService lockService;
/**
* 锁池
*/
private ThreadLocal<Map<String, DBReentrantLock>> lockPool = ThreadLocal.withInitial(Maps::newHashMap);
/**
* 获取可重入锁
*
* @param key 锁key
* @return DB锁
*/
public DBLock getReentrantLock(String key) {
String lockPath = this.getLockPath(LockType.REENTRANT_LOCK, key);
try {
Map<String, DBReentrantLock> lockPool = this.lockPool.get();
DBReentrantLock lock = lockPool.get(lockPath);
// 锁已过期,则清理
if (null != lock) {
if (lock.getExpireTime() < System.currentTimeMillis()) {
lockPool.remove(lockPath);
} else {
return lock;
}
}
lock = new DBReentrantLock(lockService, lockPath);
lockPool.put(lockPath, lock);
return lock;
} catch (Exception e) {
log.error("fail to get lock, key is {}", key, e);
throw new ServerException("this should not happen", e);
}
}
/**
* 获取锁路径
*
* @param lockType 锁类型
* @param key 锁key
* @return 锁路径
*/
private String getLockPath(LockType lockType, String key) {
return new StringBuilder()
.append("/")
.append(lockType.toString())
.append("/")
.append(key)
.toString();
}
}
4.锁行为注解
/**
* 锁行为
*
* @author ginger
* @create 2019-11-06 10:29 下午
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LockAction {
/**
* 锁资源,支持Spring EL表达式
*/
String key() default "'default'";
/**
* 锁类型,默认可重入锁
*/
LockType lockType() default LockType.REENTRANT_LOCK;
/**
* 获取锁的等待时间,默认10秒,单位对应unit()
*/
long waitTime() default 10000L;
/**
* 锁过期时间,默认1分钟,单位对应unit()
*/
long expireTime() default 60000L;
/**
* 时间单位,默认毫秒
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}
5.锁切面实现
@Slf4j
@Aspect
@Order(100)
@Component
public class LockAspect {
@Autowired
private LockContext lockContext;
/**
* Spring EL表达式解析器
*/
final private ExpressionParser parser = new SpelExpressionParser();
final private LocalVariableTableParameterNameDiscoverer discoverer =
new LocalVariableTableParameterNameDiscoverer();
@Around("@annotation(com.netease.hz.bdms.ed.service.lock.LockAction)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
LockAction lockAction = method.getAnnotation(LockAction.class);
String spel = lockAction.key();
Object[] args = pjp.getArgs();
String key = parse(spel, method, args);
DBLock lock = lockContext.getReentrantLock(key);
if (lockAction.lockType() != LockType.REENTRANT_LOCK) {
throw new LockFailureException("unsupported lock type, key is " + key);
}
if (!lock.lock(lockAction.waitTime(), lockAction.expireTime(), lockAction.unit())) {
log.debug("acquire lock unsuccessfully, key is {}", key);
throw new LockFailureException("acquire lock unsuccessfully, key is " + key);
}
log.debug("acquire lock successfully, key is {}", key);
try {
return pjp.proceed();
} finally {
lock.unlock();
}
}
private String parse(String spel, Method method, Object[] args) {
String[] params = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
if (null != params) {
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);
}
}
return parser.parseExpression(spel).getValue(context, String.class);
}
}