AOP 实现mysql分布式锁

      在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) {
        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);
  }

}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值