Token系列-第二篇-Token核心代码设计与并发控制实践

写在开始:
1️⃣ 本文仅用作个人java日常开发记录学习使用,如果涉及版权或者其他问题,及时联系小编修改或者下架,多谢
2️⃣作为Java开发者,本文将结合Token的创建/查询场景,同时回顾下关于并发锁的小问题~

一、背景回顾

  1. 前情提要
    • 首篇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分布式锁的重入特性存在两个关键设计:

  1. 实例隔离性:每次调用getReentrantLock()都会生成新的锁实例

  2. 重入条件:必须基于同一个锁实例才能实现重入

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();

}

第三部分:经验总结
  1. 锁作用域设计:入口方法统一加锁,内部方法不再重复加锁

  2. 锁实例生命周期:确保整个请求周期使用同一个锁实例

  3. API设计原则:避免public方法间的嵌套调用

  4. 监控增强:添加锁重入次数监控指标

第四部分:延伸思考

在分布式锁设计中,不同实现方案的重入机制差异较大。建议在使用前通过以下方式验证:


// 重入性验证测试用例

void testReentrant() {

    Lock lock = distributedLockManager.getReentrantLock("TEST");

    assertTrue(lock.tryLock());

    assertTrue(lock.tryLock()); // 验证二次获取是否成功

    lock.unlock();

    lock.unlock();

}

小结:分布式锁的正确使用不仅取决于锁本身的能力特性,更与调用方的使用模式密切相关。在系统设计时,需要建立锁使用规范,并通过代码审查和测试用例来保障规范落地。

写在最后 : 码字不易,如果认为不错或者对您有帮忙,希望读者动动小手,点赞或者关注哈,多谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值