写在开始:
1️⃣ 本文仅用作个人java日常开发记录学习使用,如果涉及版权或者其他问题,及时联系小编修改或者下架,多谢
2️⃣作为Java开发者,本文将结合Token的创建/查询场景,同时回顾下关于并发锁的小问题~
一、背景回顾
- 前情提要
- 首篇Token图文详解–> https://blog.csdn.net/Kaka_csdn14/article/details/147000633?spm=1011.2124.3001.6209
- 简单介绍token的概念和使用实践
二、读操作–查询伪代码分析
代码接口基本按照基础三层架构
1️⃣请求流程: Client -> Controller(控制层) -> Service(业务层) -> DAO(数据访问层) -> Database
2️⃣响应流程: Database -> DAO -> Service -> Controller -> Client
话不多说,直接code
2.1 控制层(Controller/表现层)
1-接收和处理 HTTP 和其他thrift等请求
2-参数校验和请求转发
3-异常处理和响应封装
4-不包含业务逻辑
5-负责与前端交互
package tokenproject.sdk;
import tokenproject.sdk.param.TokenCommonParam;
import tokenproject.sdk.result.TokenQueryRes;
/**
* Author: kaka-test
* Date: 2025/4/8-16:47
* ---------------------------------------
* Desc: token查询服务--提供给外部系统调用的SDK
*/
public interface ITokenQueryService {
TokenQueryRes queryToken(TokenCommonParam param);
}
package tokenproject.controller.impl;
import tokenproject.sdk.ITokenQueryService;
import tokenproject.sdk.param.TokenCommonParam;
import tokenproject.sdk.result.TokenQueryRes;
/**
* Author: kaka-test
* Date: 2025/4/8-17:05
* ---------------------------------------
* Desc: token查询服务实现类--控制层(Controller/表现层)
*/
public class TokenQueryServiceImpl implements ITokenQueryService {
// service服务注入
@Override
public TokenQueryRes queryToken(TokenCommonParam param) {
// 参数校验
// 埋点
// 查询并返回
//return TokenQueryRes.success(tokenQueryBizService.queryToken(param.getToken_code()));
}
}
相关实体类
// 查询请求参数
@Data
public class TokenCommonParam {
/**
* token编码
*/
private String token_code; //建议应该用驼峰命名
/**
* 分组ID
*/
// @NotBlank
private Integer group_id; //建议应该用驼峰命名
}
// 响应
public class TokenQueryRes extends Res<RToken> {
/**
* 无参构造
*/
public TokenQueryRes() {
}
/**
* 全参构造
*/
public TokenQueryRes( String code, String message,Boolean success, RToken result) {
super(code, message, success, result);
}
/**
* 快速构建成功响应
*/
public static TokenQueryRes success( RToken result) {
return new TokenQueryRes( "SUCCESS", "success" , Boolean.TRUE, result);
}
}
@Data
public class RToken {
private String token;
}
public abstract class Res<T> extends BaseRes{
private T result;
/**
* 无参构造
*/
public Res() {
}
public Res( String code, String msg, Boolean success,T result) {
super( code, msg,success);
this.result = result;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
}
package tokenproject.sdk.result;
/**
* 基础响应类
*/
public class BaseRes {
/**
* 交互的响应码
*/
private String code;
/**
* 交互的响应信息
*/
private String message;
/**
* 交互是否成功
*/
private Boolean success;
// 构造函数
public BaseRes() {
}
public BaseRes(String code, String message, Boolean success) {
this.code = code;
this.message = message;
this.success = success;
}
public static BaseRes success() {
return new BaseRes("SUCCESS", "success", true);
}
// Getter 和 Setter 方法
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
}
2.2 业务层(Service/服务层)
1-实现具体的业务逻辑
2-事务管理
3-数据校验和转换
4-调用多个 DAO 层完成业务
5-处理业务异常
// 接口
public interface ITokenQueryBizService {
/**
* 查询token
*/
TokenBO queryToken(String token);
}
package tokenproject.service.impl;
import tokenproject.mapper.TokenQueryDO;
import tokenproject.service.ITokenQueryBizService;
import tokenproject.service.model.TokenBO;
/**
* Author: kaka-test
* Date: 2025/4/8-17:10
* ---------------------------------------
* Desc: 这是一段伪代码
*/
//@Service
public class TokenQueryBizServiceImpl implements ITokenQueryBizService {
// DB服务注入
// @Autowired
// private TokenQueryMapper tokenQueryMapper;
// TODO: 注入缓存服务
// @Autowired
// private CacheService cacheService;
@Override
public TokenBO queryToken(String token) {
// 参数校验
if (token == null || token.isEmpty()) {
return null;
}
// 1-先查缓存
TokenBO tokenBO = null;
// tokenBO = cacheService.get(token);
// if (tokenBO != null) {
// return tokenBO;
// }
// 2-缓存未命中,再从DB查询
// TokenQueryDO tokenQueryDO = tokenQueryMapper.select(token);
// 3-DB数据不存在,数据有问题 抛异常
// 4-业务逻辑: token状态不是"使用中",不应该有查询请求 抛异常
/*if (tokenQueryDO.getStatus() != 2) { // 假设2表示使用中状态 可以用TokenStatusEnum枚举
throw Exception;
}*/
// 5-数据回写到缓存中
// 6-返回结果
return tokenBO;
}
/**
* 数据回写到缓存中 DO转BO
*/
private TokenBO tokenBasicBOConverter(TokenQueryDO tokenQueryDO) {
if (tokenQueryDO == null) {
return null;
}
TokenBO tokenBO = new TokenBO();
// TODO: 设置BO属性
return tokenBO;
}
}
// 实体
@Data
public class TokenBO {
private String token;
// 还有其他字段
}
2.3 DB层(DAO/数据访问层)
数据库操作:与数据库交互
提供数据访问方法,不包含业务逻辑
package tokenproject.mapper;
/**
* Author: test
* Date: 2025/4/8-17:13
* ---------------------------------------
* Desc: 描述该类的作用
*/
//@Repository
public interface TokenQueryMapper {
TokenQueryDO select(String token);
}
package tokenproject.mapper;
import java.util.Date;
/**
* Author: kaka-test
* Date: 2025/4/8-17:13
* ---------------------------------------
* Desc: 描述该类的作用
*/
public class TokenQueryDO {
/**
* 主键ID
*/
private Long id;
private Integer groupId;
/**
* Token账号
*/
private String tokenCode;
/**
* 状态,
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="XXXXXXX">
<!-- 结果映射 只列举一部分而已 建议mybatis直接生成-->
<resultMap id="baseResultMap"
type="tokenproject.mapper.TokenQueryDO">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="token" jdbcType="VARCHAR" property="token"/>
<!-- 结果映射 这里忽略较多字段-->
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
</resultMap>
<sql id="baseColumnList">
id, create_time, update_time
</sql>
<select id="select" resultMap="baseResultMap">
select
<include refid="baseColumnList"/>
from token_table where token = #{token, jdbcType=VARCHAR}
</select>
</mapper>
三、写操作–创建伪代码分析
3.1 控制层(Controller/表现层)
package tokenproject.sdk;
import tokenproject.sdk.param.TokenCreateParam;
import tokenproject.sdk.result.BaseRes;
/**
* Author: test
* Date: 2025/4/8-19:10
* ---------------------------------------
* Desc: token创建--提供给外部系统调用的SDK
*/
public interface ITokenCommandService {
BaseRes create(TokenCreateParam tokenCreateParam) throws Exception;
}
package tokenproject.controller.impl;
import tokenproject.sdk.ITokenCommandService;
import tokenproject.sdk.param.TokenCreateParam;
import tokenproject.sdk.result.BaseRes;
/**
* Author: test
* Date: 2025/4/8-19:13
* ---------------------------------------
* Desc: token创建实现类--控制层(Controller/表现层)
*/
public class TokenCommandServiceImpl implements ITokenCommandService {
@Override
public BaseRes create(TokenCreateParam tokenCreateParam) throws Exception {
// 参数校验
// 参数转换 TokenCreateParam转为TokenCreateDTO
// 调用业务进行创建
return BaseRes.success();
}
}
// 创建实体请求参数
@Data
public class TokenCreateParam {
private String tokenCode;
// 其他字段
}
3.2 写操作业务层(Service/服务层)
package tokenproject.service;
import tokenproject.service.model.TokenCreateDTO;
/**
* Token 写操作业务服务接口
* 负责处理 Token 相关的写操作业务逻辑
*/
public interface ITokenCommandBizService {
/**
* 创建新的 Token
*
* @param tokenCreateDTO Token创建参数DTO,包含创建Token所需的必要信息
* @throws Exception 创建过程中可能出现的异常
*/
void createToken(TokenCreateDTO tokenCreateDTO) throws Exception;
}
package tokenproject.service.impl;
import lombok.extern.slf4j.Slf4j;
import tokenproject.service.ITokenCommandBizService;
import tokenproject.service.model.TokenCreateDTO;
import tokenproject.service.utils.DistributedLockUtil;
import java.util.concurrent.locks.Lock;
/**
* Token操作业务服务实现类核心逻辑--创建token举例
*/
@Slf4j
public class TokenCommandBizServiceImpl implements ITokenCommandBizService {
// 注入tokenMapper
private static final String LOCK_KEY_PREFIX = "token_";
@Override
public void createToken(TokenCreateDTO tokenCreateDTO) throws Exception {
concurrentOperationControl(tokenCreateDTO.getTokenCode(), () -> {
// 1 根据tokenCode查询,如果已存在,则抛出异常
// 2 其余业务诉求校验,比如根据 根据业务身份查询token信息,如果已存在 抛异常
// 3 调mapper创建token dto转tokenDO
/*boolean result = tokenMapper.insert(TokenDOConverter.of(tokenCreateDTO)) == 1;
log.info("[Token变更后]-Token账号={}-创建token结果={}",tokenCreateDTO.getTokenCode(), result);
if (!result) {
throw ExceptionCode.TOKEN_CREATE_FAILED.newException("创建token失败,tokenCode={}", tokenCreateDTO.getTokenCode());
}*/
}
);
}
void concurrentOperationControl(String tokenCode, BizFunction function) throws Exception {
String lockName = LOCK_KEY_PREFIX + tokenCode;
Lock lock = DistributedLockUtil.getReentrantLock(lockName);
if (lock.tryLock()) {
try {
//log.info("Lock acquired, tokenCode={}", tokenCode);
function.apply();
} finally {
lock.unlock();
//log.info("Lock released, tokenCode={}", tokenCode);
}
} else {
//log.info("Try lock failed tokenCode={}", tokenCode);
// 打日志 抛异常
//throw ExceptionCode.CONCURRENT_OPERATION_REJECTED.newException("当前有操作正在进行中,请稍后重试");
}
}
}
@FunctionalInterface
public interface BizFunction {
void apply() throws Exception;
}
// 锁逻辑实现举例
package tokenproject.service.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
/**
* 分布式锁工具类
*/
public class DistributedLockUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(DistributedLockUtil.class);
private StringRedisTemplate redisTemplate;
private String appKey = "";
private static final int DEFAULT_EXPIRE_TIME = 30000; // 默认30秒过期
private static final int DEFAULT_RETRY_TIMES = 3; // 默认重试3次
public void init() {
checkProperty();
LOGGER.info("DistributedLockUtil initialized successfully");
}
private void checkProperty() {
if (appKey == null || appKey.trim().isEmpty()) {
String error = "Incoming appKey is invalid! Please check the appKey property.";
LOGGER.error(error);
throw new RuntimeException(error);
}
}
/**
* 获取可重入锁
*/
public static Lock getReentrantLock(String lockName) {
return getReentrantLock(lockName, DEFAULT_EXPIRE_TIME);
}
/**
* 获取可重入锁(指定过期时间)
*/
public static Lock getReentrantLock(String lockName, int expireTime) {
return getReentrantLock(lockName, expireTime, DEFAULT_RETRY_TIMES);
}
/**
* 获取可重入锁(指定过期时间和重试次数)
*/
public static Lock getReentrantLock(String lockName, int expireTime, int retry) {
String finalLockName = appKey + ":" + lockName;
return new RedisLock(redisTemplate, finalLockName, expireTime, retry);
}
/**
* 获取读写锁
*/
public ReadWriteLock getReentrantReadWriteLock(String lockName) {
return getReentrantReadWriteLock(lockName, DEFAULT_EXPIRE_TIME);
}
/**
* 获取读写锁(指定过期时间)
*/
public ReadWriteLock getReentrantReadWriteLock(String lockName, int expireTime) {
String finalLockName = appKey + ":" + lockName;
return new RedisReadWriteLock(redisTemplate, finalLockName, expireTime);
}
public void destroy() {
LOGGER.info("DistributedLockUtil destroyed successfully");
}
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public String getAppKey() {
return appKey;
}
}
package tokenproject.service.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
public class RedisLock implements Lock {
private final StringRedisTemplate redisTemplate;
private final String lockKey;
private final int expireTime;
private final int retryTimes;
private final String lockValue;
public RedisLock(StringRedisTemplate redisTemplate, String lockKey, int expireTime, int retryTimes) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.expireTime = expireTime;
this.retryTimes = retryTimes;
this.lockValue = Thread.currentThread().getId() + ":" + System.nanoTime();
}
@Override
public void lock() {
int count = 0;
while (!tryLock()) {
count++;
if (count >= retryTimes) {
throw new RuntimeException("Failed to acquire lock: " + lockKey);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Lock interrupted", e);
}
}
}
@Override
public boolean tryLock() {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
3.3 DB层(DAO/数据访问层)忽略
四、锁关键代码解析
4.1 锁的背景
为了避免对token的并发写操作引发问题,在service入口层加入了分布式锁(每个public方法入口都加入了分布式锁判断),使用的是tryLock的方式。具体可以见 3.2 写操作业务层(Service/服务层)
// 每个public方法入口处添加锁判断
public void publicMethodA() {
Lock lock = lockService.getReentrantLock(lockName);
if (!lock.tryLock()) {
throw new ConcurrentOperationException("不允许并发操作");
}
// ...业务逻辑
}
4.2 锁问题的现象
没有锁竞争(仅发起单次请求的情况),但是却收到报错提示“不允许并发操作:当前有操作正在进行中,请稍后重试”。
4.3 原因分析
一个public方法既是外部直接调用的方法,又会被其他public方法间接调用,两个public方法入口都加入了分布式锁控制,所以实际上分布式锁被调用了两次。相当于锁重入了,但是Cerberus实际没有重入。导致第一次调用时获取到了锁,第二次调用获取不到锁,于是就报错了。
Cerberus的分布式锁实际上也支持重入,但是它的可重入特性容易引起误解。
4.3.1 方法调用链分析
@PublicApi
public void outerMethod() {
lockCheck(); // 第一次获取锁
innerMethod();
}
@PublicApi
public void innerMethod() {
lockCheck(); // 第二次获取锁
}
private void lockCheck() {
Lock lock = distributedLockManager.getReentrantLock("SAME_LOCK");
if (!lock.tryLock()) throw new ConcurrentOperationException(...);
}
4.3.2 锁实现机制
Cerberus分布式锁的重入特性存在两个关键设计:
-
实例隔离性:每次调用
getReentrantLock()
都会生成新的锁实例 -
重入条件:必须基于同一个锁实例才能实现重入
4.3.3 问题本质
调用层级 | 锁实例 | 加锁结果 |
---|---|---|
外层调用 | LockA | 成功获取 |
内层调用 | LockB | 失败(非重入) |
语句:
Lock lock = lockService.getReentrantLock(lockName);
每一次get都会是一个新的锁对象,而不是之前已有的,所以如果每次锁判断前都get,则无法重入。需要重入的话,得显式传递lock对象的引用。
4.3.4 建议解决方法
避免在一次请求中多次尝试获取锁;
其余思考:
第一部分: 锁实例管理优化
// 使用Holder模式统一管理锁实例
public class LockHolder {
private static final ConcurrentHashMap<String, Lock> lockMap = new ConcurrentHashMap<>();
public static Lock getLock(String lockName) {
return lockMap.computeIfAbsent(lockName,
k -> distributedLockManager.getReentrantLock(k));
}
}
第二部分: 调用链改造
// 改造后的方法调用
@PublicApi
public void entryMethod() {
Lock lock = LockHolder.getLock("GLOBAL_LOCK");
if (lock.tryLock()) {
try {
businessLogic();
} finally {
lock.unlock();
}
}
}
private void businessLogic() {
// 原public方法改为private
methodA();
methodB();
}
第三部分:经验总结
-
锁作用域设计:入口方法统一加锁,内部方法不再重复加锁
-
锁实例生命周期:确保整个请求周期使用同一个锁实例
-
API设计原则:避免public方法间的嵌套调用
-
监控增强:添加锁重入次数监控指标
第四部分:延伸思考
在分布式锁设计中,不同实现方案的重入机制差异较大。建议在使用前通过以下方式验证:
// 重入性验证测试用例
void testReentrant() {
Lock lock = distributedLockManager.getReentrantLock("TEST");
assertTrue(lock.tryLock());
assertTrue(lock.tryLock()); // 验证二次获取是否成功
lock.unlock();
lock.unlock();
}
小结:分布式锁的正确使用不仅取决于锁本身的能力特性,更与调用方的使用模式密切相关。在系统设计时,需要建立锁使用规范,并通过代码审查和测试用例来保障规范落地。
写在最后 : 码字不易,如果认为不错或者对您有帮忙,希望读者动动小手,点赞或者关注哈,多谢