分布式锁业务实战

446 阅读6分钟

简介

这篇文章记录了实际线上业务,分布式锁的引入原因,引入过程以及引入过程发生的一些问题,包含了我个人的一些思考和具体的代码实现,从一开始的救火分布式锁,到最后优雅的使用分布式锁,希望能给大家提供一些思路。

第一次写文章,希望大家多多指教。

业务场景

我们的业务对接了一个App,支持这个App账户直接跳转,我们这边为这个app账户生成一个系统虚拟用户。 这个业务涉及两张表,分别是媒体账户表AppAccount,以及系统用户表User表。AppAccount表用于将各种App账户绑定到我们的系统用户User上,User则是我们系统的登陆账号。

业务流程图

image.png

简化代码:

@Transactional(rollbackFor = Exception.class)
public String loginByUid(String uid) {
    if (isNotGenerated(uid)) {
        generateUser(uid);
        generateAppAccount(uid);
    }
    return getToken(uid);
}

线上问题

这个功能上线不过一个小时,就发现线上出现了很多错误数据,AppAccount表出现了很多相同的App账户。 根据线上的报错日志和access-log,很轻松地找到了问题的原因。我们发现App端会同时发送多条相同的loginByUid请求,这个接口并没有保证幂等性,就导致出现了重复的AppAccount数据。

原因分析

线上服务一般都是部署多台,保证高可用。

当相同请求在同一时间打到多个服务,服务首先会先执行isNotGenerated,去查看该App账户是否已经生成了系统用户,发现没有生成,那么同时都进入到了generate代码块,这就导致了AppAccount表出现多个相同账户。

解决方式

施加分布式锁,由于当时时间比较紧急,来不及申请其他资源,就使用了最业务数据库实现了分布式锁。

技术方案

数据库新增表DistributedLock

create table DistributedLock
(
    ID       bigint auto_increment
        primary key,
    LOCK_KEY varchar(255) null,
    constraint DistributedLock_LOCK_KEY_uindex
        unique (LOCK_KEY)
);

使用数据库做分布式锁的方案主要有两种

基于插入删除

  1. 插入LOCK_KEY,插入成功则获得锁
  2. 业务逻辑结束后,将LOCK_KEY删除
image.png

优点

  1. 实现简单
  2. 不需要额外引入其他中间件,使用业务数据库即可

缺点

  1. 没有重试机制,没获取到锁就直接失败
  2. 死锁问题

基于for update

  1. 开启事务
  2. 查看LOCK_KEY是否存在,不存在则插入
  3. 使用'SELECT * FROM DistributedLock WHERE LOCK_KEY=? for update' 加锁
  4. 执行业务逻辑
  5. 关闭事务

优点

  1. 实现相对简单
  2. 不需要额外引入其他中间件
  3. 不会死锁

缺点

  1. for update可能导致系统响应超时
image.png

最终我选择了for update的分布式锁方案。

代码

public interface DistributedLockService extends IService<DistributedLock> {

    void lock(String param);
    
    void unlock(String param);
}
@Service
public class DistributedLockServiceImpl extends ServiceImpl<DistributedLockMapper, DistributedLock>
implements DistributedLockService {

    @Override
    public void lock(String lockKey) {
        DistributedLock lock = new DistributedLock();
        lock.setLockKey(lockKey);
        try {
            if (!new LambdaQueryChainWrapper<>(baseMapper)
                    .eq(DistributedLock::getLockKey, lockKey)
                    .exists()) {
                this.save(lock);
            }
        } catch (Exception e) {
            ...
        }
        baseMapper.lock(lockKey);
    }
    
    @Override
    public void unLock() {

    }
}
public interface DistributedLockMapper extends BaseMapper<DistributedLock> {
    @Select("select ID from DistributedLock where LOCK_KEY=#{param} for update")
    DistributedLock lock(String param);
}
@Transactional(rollbackFor = Exception.class)
public String loginByUid(String uid) {
    lockService.lock(uid);
    if (isNotGenerated(uid)) {
        generateUser(uid);
        generateAppAccount(uid);
    }
    return getToken(uid);
}

分布式锁升级优化

当然上述的方案只是紧急救火,后续我又对系统的分布式锁实现进行了升级。

  1. 采用ZK作为分布式锁的实现
  2. 支持分布式互斥锁,分布式读写锁
  3. 支持使用注解的形式加锁,将业务与技术解耦,同时方便使用

使用ZK改写

由于之前代码写得还比较易扩展,所以直接实现了ZkDistributedLockServiceImpl,对系统整体地功能没有太大地改变。

引入zk相关依赖

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>xxx</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>xxx</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>xxx</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
    </exclusions>
</dependency>

实现ZkDistributedLockServiceImpl

public interface DistributedLockService {

    /**
     * 可重入互斥锁 加锁
     * @param lockKey -
     */
    void mutexLock(String lockKey);
    /**
     * 可重入互斥锁 加锁
     * @param lockKey -
     * @param time 获取锁等待最大时间
     * @param timeUnit 时间单位
     */
    void mutexLock(String lockKey, Integer time, TimeUnit timeUnit);

    /**
     * 解锁
     * @param lockKey -
     */
    void unlock(String lockKey);

    /**
     * 读写锁加锁
     * @param lockKey -
     * @param lockType {@link DistributedLock}
     */
    void readWriteLock(String lockKey, Integer lockType);

    void readWriteLock(String lockKey, Integer time, TimeUnit timeUnit, Integer lockType);

    void readWriteUnLock(String lockKey, Integer lockType);
}
@Service
@Slf4j
@RequiredArgsConstructor
public class ZkDistributedLockServiceImpl implements DistributedLockService {
    private final CuratorFramework client;

    @Value("${zookeeper.lock.path}")
    private String lockBasePath;

    private final ThreadLocal<Map<String, Pair<InterProcessLock, Integer>>> MUTEX_LOCK_MAP = new ThreadLocal<>();
    private final ThreadLocal<Map<String, Pair<InterProcessReadWriteLock, Integer>>> READ_WRITE_LOCK_MAP = new ThreadLocal<>();

    @Override
    public void mutexLock(String lockKey) {
        this.mutexLock(lockKey, -1, null);
    }

    @Override
    public void mutexLock(String lockKey, Integer time, TimeUnit timeUnit) {
        try {
            Map<String, Pair<InterProcessLock, Integer>> map = MUTEX_LOCK_MAP.get();
            if (Objects.isNull(map)) {
                map = new HashMap<>();
            }
            InterProcessLock lock;
            Pair<InterProcessLock, Integer> pair = map.get(lockKey);
            if (Objects.isNull(pair)) {
                lock = new InterProcessMutex(client, lockBasePath + "/" + lockKey);
            } else {
                lock = pair.getKey();
            }
            lock.acquire(time, timeUnit);
            pair = new Pair<>(lock, pair != null ? pair.getValue() + 1 : 1);
            map.put(lockKey, pair);
            MUTEX_LOCK_MAP.set(map);
        } catch (Exception e) {
            ...
        }
    }

    @Override
    public void unlock(String lockKey) {
        try {
            Map<String, Pair<InterProcessLock, Integer>> map = MUTEX_LOCK_MAP.get();
            assert map != null;
            Pair<InterProcessLock, Integer> pair = map.get(lockKey);
            assert pair != null;
            InterProcessLock lock = pair.getKey();
            lock.release();
            int count = pair.getValue() - 1;
            if (count > 0) {
                pair = new Pair<>(lock, count);
                map.put(lockKey, pair);
                MUTEX_LOCK_MAP.set(map);
            } else {
                map.remove(lockKey);
                if (map.isEmpty()) {
                    MUTEX_LOCK_MAP.remove();
                }
            }
        } catch (Exception e) {
            ...
        }
    }

    @Override
    public void readWriteLock(String lockKey, Integer lockType) {
        this.readWriteLock(lockKey, -1, null, lockType);
    }

    @Override
    public void readWriteLock(String lockKey, Integer time, TimeUnit timeUnit, Integer lockType) {
        try {
            Map<String, Pair<InterProcessReadWriteLock, Integer>> map = READ_WRITE_LOCK_MAP.get();
            if (Objects.isNull(map)) {
                map = new HashMap<>();
            }
            InterProcessReadWriteLock lock;
            Pair<InterProcessReadWriteLock, Integer> pair = map.get(lockKey);
            if (Objects.isNull(pair)) {
                lock = new InterProcessReadWriteLock(client, lockBasePath + "/" + lockKey);
            } else {
                lock = pair.getKey();
            }
            if (lockType.equals(DistributeLock.READ_LOCK)) {
                lock.readLock().acquire();
            } else if (lockType.equals(DistributeLock.WRITE_LOCK)) {
                lock.writeLock().acquire();
            } else {
                ...
            }
            pair = new Pair<>(lock, pair != null ? pair.getValue() + 1 : 1);
            map.put(lockKey, pair);
            READ_WRITE_LOCK_MAP.set(map);
        } catch (Exception e) {
            ...
        }
    }

    @Override
    public void readWriteUnLock(String lockKey, Integer lockType) {
        try {
            Map<String, Pair<InterProcessReadWriteLock, Integer>> map = READ_WRITE_LOCK_MAP.get();
            assert map != null;
            Pair<InterProcessReadWriteLock, Integer> pair = map.get(lockKey);
            assert pair != null;
            InterProcessReadWriteLock lock = pair.getKey();

            if (lockType.equals(DistributeLock.READ_LOCK)) {
                lock.readLock().acquire();
            } else if (lockType.equals(DistributeLock.WRITE_LOCK)) {
                lock.writeLock().acquire();
            } else {
                ...
            }
            int count = pair.getValue() - 1;
            if (count > 0) {
                pair = new Pair<>(lock, count);
                map.put(lockKey, pair);
                READ_WRITE_LOCK_MAP.set(map);
            } else {
                map.remove(lockKey);
                if (map.isEmpty()) {
                    READ_WRITE_LOCK_MAP.remove();
                }
            }
        } catch (Exception e) {
            ...
        }
    }
}

业务代码改写

@Transactional(rollbackFor = Exception.class)
public String loginByUid(String uid) {
    lockService.lock(uid);
    if (isNotGenerated(uid)) {
        generateUser(uid);
        generateAppAccount(uid);
    }
    lockService.unLock(uid);
    return getToken(uid);
}

注解实现

主要有两个注解 DistributedLock 以及 DistributedLockKey。DistributedLock标记需要施加分布式锁的方法,DistributedLockKey标记哪些参数作为lockKey进行加锁。

以下是代码实现:

注解

/**
 * 分布式锁注解<p>
 * 可以配合{@link DistributedLockKey}使用
 * @author chenjiahui
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DistributedLock {
    int MUTEX_LOCK = 0;
    int READ_LOCK = 1;
    int WRITE_LOCK = 2;

    String namespace() default "default-lock";

    int time() default -1;

    TimeUnit timeUnit() default TimeUnit.SECONDS;

    int lockType() default MUTEX_LOCK;
}
/**
 * 分布式锁 lock key
 * 需要配合{@link DistributedLock}使用
 *
 * <pre>
 *     {@code
 *     @DistributedLock(namespace="obj")
 *     createObj(
 *              @DistributedLockKey(order=0) String key1,
 *              @DistributedLockKey(order=1) String key2,
 *              @DistributedLockKey String key3
 *              ){
 *                  // code
 *              }
 *     }
 * </pre>
 * 在执行code前,会使用以下字符串作为lock_key加锁,basePath/obj-key1key2key3
 *
 * <pre>
 *     {@code
 *     class Data{
 *         @DistributedLockKey
 *         private String key1 = "key1";
 *         @DistributedLockKey
 *         private String key2 = "key2";
 *     }
 *     @DistributedLock(namespace="obj")
 *     createObj(
 *              @DistributedLockKey(order=0) Data obj,
 *              ){
 *                  // code
 *              }
 *     }
 * </pre>
 * {@link DistributedLockKey} 支持递归查找,在执行code前,会使用以下字符串作为lock_key加锁,basePath/obj-key1key2
 * @author chenjiahui
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface DistributedLockKey {
    /**
     * order 用于拼接 lock key,order越小排序越前面
     */
    int order() default Integer.MAX_VALUE;
}

AOP

接下来是Aop的具体实现:

代码逻辑也比较简单

  1. 通过反射,获取到业务方法的含有DistributedLockKey注解的入参,根据规则拼接成lockKey
  2. 根据distributedLock.lockType(),确定业务需要施加的锁是互斥锁还是读写锁,调用DistributedLockService相应的方法进行加锁
  3. 在业务执行完后,在调用DistributedLockService unlock方法进行解锁即可
/**
 * @author chenjiahui
 * @date 2022年07月21日 16:35
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class DistributedLockAspect {
    private final DistributedLockService distributedLockService;

    @Pointcut("@annotation(distributedLock)")
    public void distributedLockAspect(DistributedLock distributedLock) {
    }

    @Around(value = "distributedLockAspect(distributedLock)", argNames = "joinPoint,distributedLock")
    public Object distributedLock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        String lockKey = getLockKey(joinPoint, distributedLock.namespace());
        try {
            if (distributedLock.time() != -1) {
                if (distributedLock.lockType() == DistributedLock.MUTEX_LOCK) {
                    distributedLockService.mutexLock(lockKey, distributedLock.time(), distributedLock.timeUnit());
                } else {
                    distributedLockService.readWriteLock(lockKey, distributedLock.time(), distributedLock.timeUnit(), distributedLock.lockType());
                }
            } else {
                if (distributedLock.lockType() == DistributedLock.MUTEX_LOCK) {
                    distributedLockService.mutexLock(lockKey);
                } else {
                    distributedLockService.readWriteLock(lockKey, distributedLock.lockType());
                }
            }
            return joinPoint.proceed();
        } finally {
            if (distributedLock.lockType() == DistributedLock.MUTEX_LOCK) {
                distributedLockService.unlock(lockKey);
            } else {
                distributedLockService.readWriteUnLock(lockKey, distributedLock.lockType());
            }
        }
    }

    private String getLockKey(ProceedingJoinPoint joinPoint, String namespace) throws IllegalAccessException {
        Map<Integer, List<String>> order2Keys = new TreeMap<>(Integer::compareTo);
        Object[] params = joinPoint.getArgs();
        if (params.length == 0) {
            return namespace;
        }
        //获取方法,此处可将signature强转为MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        Annotation[][] annotations = method.getParameterAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            Object param = params[i];
            Annotation[] paramAnn = annotations[i];
            if (param == null || paramAnn.length == 0) {
                continue;
            }
            for (Annotation annotation : paramAnn) {
                if (annotation.annotationType().equals(DistributedLockKey.class)) {
                    DistributedLockKey d = (DistributedLockKey) annotation;
                    if (param instanceof String) {
                        order2Keys.computeIfAbsent(d.order(), k -> new ArrayList<>()).add((String) param);
                    } else {
                        recursionFindLockKey(order2Keys, param);
                    }
                }
            }
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<Integer, List<String>> next : order2Keys.entrySet()) {
            for (String s : next.getValue()) {
                sb.append(s);
            }
        }
        return StringUtils.joinWith("_", "xxx", namespace, sb);
    }

    private void recursionFindLockKey(Map<Integer, List<String>> order2Keys, Object param) throws IllegalAccessException {
        if (Objects.isNull(param)) {
            return;
        }
        Class<?> clazz = param.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            DistributedLockKey annotation = field.getAnnotation(DistributedLockKey.class);
            if (Objects.nonNull(annotation)) {
                field.setAccessible(true);
                Object o = field.get(param);
                if (o instanceof String) {
                    order2Keys.computeIfAbsent(annotation.order(), k -> new ArrayList<>()).add((String) o);
                } else {
                    recursionFindLockKey(order2Keys, o);
                }
            }
        }
    }
}

业务代码改写

@Transactional(rollbackFor = Exception.class)
@DistributedLock(namespace = "appAccount")
public String loginByUid(@DistributedLockKey String uid) {
    if (isNotGenerated(uid)) {
        generateUser(uid);
        generateAppAccount(uid);
    }
    return getToken(uid);
}

问题

这一版本改完之后,本来想着这个功能也就完美实现了,结果一上线,又出现了重复插入的问题。 这就让我有点凌乱了,按道理来说不应该出现问题,但是他确实是出现问题了,所以我又开始了线上问题排查之旅。

原因

我们来看下升级前后加锁的逻辑问题。

image.pngimage.png

由于之前是采用数据库for update加锁,不需要显式的解锁,只需要关闭事务就默认关闭了锁。

而新的方案,目前的写法如图二,是解锁后调用的关闭事务,那么这会导致什么问题呢?

由于分布式锁解锁,事务还未关闭,所以事务对数据库的修改并未生效,在并发的情况下,其余线程此时可以获得锁,执行业务逻辑的过程中,发现isNotGenerated(uid)为true,所以又进行了一次生成操作。

修改方案

改变加锁解锁的时机,将DistributedLock的Aop优先级调成最高级

image.png

代码

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class DistributedLockAspect implements Ordered {
    ...
    @Override
    public int getOrder() {
        return 0;
    }
    ...
}

小结

以上就是我在业务中引入分布式锁的全过程了,希望各位大佬多多赐教。