简介
这篇文章记录了实际线上业务,分布式锁的引入原因,引入过程以及引入过程发生的一些问题,包含了我个人的一些思考和具体的代码实现,从一开始的救火分布式锁,到最后优雅的使用分布式锁,希望能给大家提供一些思路。
第一次写文章,希望大家多多指教。
业务场景
我们的业务对接了一个App,支持这个App账户直接跳转,我们这边为这个app账户生成一个系统虚拟用户。 这个业务涉及两张表,分别是媒体账户表AppAccount,以及系统用户表User表。AppAccount表用于将各种App账户绑定到我们的系统用户User上,User则是我们系统的登陆账号。
业务流程图:
简化代码:
@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)
);
使用数据库做分布式锁的方案主要有两种
基于插入删除
- 插入LOCK_KEY,插入成功则获得锁
- 业务逻辑结束后,将LOCK_KEY删除
优点
- 实现简单
- 不需要额外引入其他中间件,使用业务数据库即可
缺点
- 没有重试机制,没获取到锁就直接失败
- 死锁问题
基于for update
- 开启事务
- 查看LOCK_KEY是否存在,不存在则插入
- 使用'SELECT * FROM DistributedLock WHERE LOCK_KEY=? for update' 加锁
- 执行业务逻辑
- 关闭事务
优点
- 实现相对简单
- 不需要额外引入其他中间件
- 不会死锁
缺点
- for update可能导致系统响应超时
最终我选择了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);
}
分布式锁升级优化
当然上述的方案只是紧急救火,后续我又对系统的分布式锁实现进行了升级。
- 采用ZK作为分布式锁的实现
- 支持分布式互斥锁,分布式读写锁
- 支持使用注解的形式加锁,将业务与技术解耦,同时方便使用
使用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的具体实现:
代码逻辑也比较简单
- 通过反射,获取到业务方法的含有DistributedLockKey注解的入参,根据规则拼接成lockKey
- 根据distributedLock.lockType(),确定业务需要施加的锁是互斥锁还是读写锁,调用DistributedLockService相应的方法进行加锁
- 在业务执行完后,在调用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);
}
问题
这一版本改完之后,本来想着这个功能也就完美实现了,结果一上线,又出现了重复插入的问题。 这就让我有点凌乱了,按道理来说不应该出现问题,但是他确实是出现问题了,所以我又开始了线上问题排查之旅。
原因
我们来看下升级前后加锁的逻辑问题。
由于之前是采用数据库for update加锁,不需要显式的解锁,只需要关闭事务就默认关闭了锁。
而新的方案,目前的写法如图二,是解锁后调用的关闭事务,那么这会导致什么问题呢?
由于分布式锁解锁,事务还未关闭,所以事务对数据库的修改并未生效,在并发的情况下,其余线程此时可以获得锁,执行业务逻辑的过程中,发现isNotGenerated(uid)为true,所以又进行了一次生成操作。
修改方案
改变加锁解锁的时机,将DistributedLock的Aop优先级调成最高级
代码
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class DistributedLockAspect implements Ordered {
...
@Override
public int getOrder() {
return 0;
}
...
}
小结
以上就是我在业务中引入分布式锁的全过程了,希望各位大佬多多赐教。