短链接day9

功能扩展

用户创建分组限制最大数量

使用redissonClient(分布式锁实现)

短链接验证布隆过滤器域名冲突

使用的是默认的域名,createShortLinkDefaultDomain。

公网环境部署系统如何做流量风控

短链接后管:

根据登录用户做出控制,比如 x 秒请求后管系统的频率最多 x 次。

实现原理也比较简单,通过 Redis increment 命令对一个数据进行递增,如果超过 x 次就会返回失败。这里有个细节就是我们的这个周期是 x 秒,需要对 Redis 的 Key 设置 x 秒有效期。

但是 Redis 中对于 increment 命令是没有提供过期命令的,这就需要两步操作,进而出现原子性问题。

为此,我们需要通过 LUA 脚本来保证原子性。

-- 设置用户访问频率限制的参数
local username = KEYS[1]
local timeWindow = tonumber(ARGV[1]) -- 时间窗口,单位:秒

-- 构造 Redis 中存储用户访问次数的键名
local accessKey = "short-link:user-flow-risk-control:" .. username

-- 原子递增访问次数,并获取递增后的值
local currentAccessCount = redis.call("INCR", accessKey)

-- 设置键的过期时间
redis.call("EXPIRE", accessKey, timeWindow)

-- 返回当前访问次数
return currentAccessCount

yml

short-link:
  flow-limit:
    enable: true
    time-window: 1
    max-access-count: 20

cofig

@Data
@Component
@ConfigurationProperties(prefix = "short-link.flow-limit")
public class UserFlowRiskControlConfiguration {

    /**
     * 是否开启用户流量风控验证
     */
    private Boolean enable;

    /**
     * 流量风控时间窗口,单位:秒
     */
    private String timeWindow;

    /**
     * 流量风控时间窗口内可访问次数
     */
    private Long maxAccessCount;
}

common.biz.user 

@Slf4j
@RequiredArgsConstructor
public class UserFlowRiskControlFilter implements Filter {

    private final StringRedisTemplate stringRedisTemplate;
    private final UserFlowRiskControlConfiguration userFlowRiskControlConfiguration;

    private static final String USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH = "lua/user_flow_risk_control.lua";

    @SneakyThrows
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH)));
        redisScript.setResultType(Long.class);
        String username = Optional.ofNullable(UserContext.getUsername()).orElse("other");
        Long result;
        try {
            result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(username), userFlowRiskControlConfiguration.getTimeWindow());
        } catch (Throwable ex) {
            log.error("执行用户请求流量限制LUA脚本出错", ex);
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        if (result == null || result > userFlowRiskControlConfiguration.getMaxAccessCount()) {
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        filterChain.doFilter(request, response);
    }

    private void returnJson(HttpServletResponse response, String json) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(json);
        }
    }
}

短链接中台:

根据接口进行流控,比如同一接口最大接受 20 QPS。

1. 引入 Sentinel

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>

2. 定义接口规则

定义需要风控接口的规则。

package com.nageoffer.shortlink.project.config;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 初始化限流配置
 */
@Component
public class SentinelRuleConfig implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule createOrderRule = new FlowRule();
        createOrderRule.setResource("create_short-link");
        createOrderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        createOrderRule.setCount(1);
        rules.add(createOrderRule);
        FlowRuleManager.loadRules(rules);
    }
}

如果触发风控,设置降级策略。

package com.nageoffer.shortlink.project.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.nageoffer.shortlink.project.common.convention.result.Result;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkCreateRespDTO;

/**
 * 自定义流控策略
 */
public class CustomBlockHandler {

    public static Result<ShortLinkCreateRespDTO> createShortLinkBlockHandlerMethod(ShortLinkCreateReqDTO requestParam, BlockException exception) {
        return new Result<ShortLinkCreateRespDTO>().setCode("B100000").setMessage("当前访问网站人数过多,请稍后再试...");
    }
}

在代码中引入 Sentinel 注解控制流控规则。

/**
 * 创建短链接
 */
@PostMapping("/api/short-link/v1/create")
@SentinelResource(
        value = "create_short-link",
        blockHandler = "createShortLinkBlockHandlerMethod",
        blockHandlerClass = CustomBlockHandler.class
)
public Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam) {
    return Results.success(shortLinkService.createShortLink(requestParam));
}

3. 微服务版本 Sentinel 如何接入?

dashboard | Sentinel

启动 Sentinel 控制台,删除 Sentinel 定义的相关规则代码,加入以下配置即可。
删除的规则配置,在 Sentinel 中进行配置。

spring:
    sentinel:
      transport:
        dashboard: localhost:8686
        port: 8719

4. 压测脚本

jmx

消息队列重构短链接监控功能

海量访问短链接,直接访问数据库,会导致数据库负载变高,甚至数据库宕机。为此,需要引入消息队列削峰。

消息队列使用场景:从零到一学习RocketMQ | 拿个offer - 开源&项目实战

1. 为什么使用 Redis 充当消息队列?

轻量级(这里已经使用了redis作为缓存,为了减少组件的引入,所以这里用redis作消息队列)

2. Redis 实现消息队列的几种方式?

List、PubSub、Stream

使用 Redis 充当消息队列参考文章:Redis消息队列发展历程

3. 使用 Redis 消息队列后逻辑

未使用时:

使用后:

创建 Redis Stream Key 相关配置

在Redis Desktop Manager中写入命令

1. 创建 Stream Key

XADD "short_link:stats-stream" * "New key" "New value"

2. 创建消费者组

xgroup create short_link:stats-stream short_link:stats-stream:only-group 0

使用消息队列后的一些问题?

数据延迟、幂等

代码:

config

/**
 * Redis Stream 消息队列配置
 */
@Configuration
@RequiredArgsConstructor
public class RedisStreamConfiguration {

    private final RedisConnectionFactory redisConnectionFactory;
    private final ShortLinkStatsSaveConsumer shortLinkStatsSaveConsumer;

    @Bean
    public ExecutorService asyncStreamConsumer() {
        AtomicInteger index = new AtomicInteger();
        int processors = Runtime.getRuntime().availableProcessors();
        return new ThreadPoolExecutor(processors,
                processors + processors >> 1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                runnable -> {
                    Thread thread = new Thread(runnable);
                    thread.setName("stream_consumer_short-link_stats_" + index.incrementAndGet());
                    thread.setDaemon(true);
                    return thread;
                }
        );
    }

    @Bean(initMethod = "start", destroyMethod = "stop")
    public StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer(ExecutorService asyncStreamConsumer) {
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
                StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                        .builder()
                        // 一次最多获取多少条消息
                        .batchSize(10)
                        // 执行从 Stream 拉取到消息的任务流程
                        .executor(asyncStreamConsumer)
                        // 如果没有拉取到消息,需要阻塞的时间。不能大于 ${spring.data.redis.timeout},否则会超时
                        .pollTimeout(Duration.ofSeconds(3))
                        .build();
        StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer =
                StreamMessageListenerContainer.create(redisConnectionFactory, options);
        streamMessageListenerContainer.receiveAutoAck(Consumer.from(SHORT_LINK_STATS_STREAM_GROUP_KEY, "stats-consumer"),
                StreamOffset.create(SHORT_LINK_STATS_STREAM_TOPIC_KEY, ReadOffset.lastConsumed()), shortLinkStatsSaveConsumer);
        return streamMessageListenerContainer;
    }
}

 yml

 更改impl中的代码

发送流程:

 mq/producer

/**
 * 短链接监控状态保存消息队列生产者
 */
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveProducer {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 发送延迟消费短链接统计
     */
    public void send(Map<String, String> producerMap) {
        stringRedisTemplate.opsForStream().add(SHORT_LINK_STATS_STREAM_TOPIC_KEY, producerMap);
    }
}

mq/consumer

/**
 * 短链接监控状态保存消息队列消费者
 * 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:link)获取项目资料
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ShortLinkStatsSaveConsumer implements StreamListener<String, MapRecord<String, String, String>> {

    private final ShortLinkMapper shortLinkMapper;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final MessageQueueIdempotentHandler messageQueueIdempotentHandler;

    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Override
    public void onMessage(MapRecord<String, String, String> message) {
        String stream = message.getStream();
        RecordId id = message.getId();
        if (messageQueueIdempotentHandler.isMessageBeingConsumed(id.toString())) {
            // 判断当前的这个消息流程是否执行完成
            if (messageQueueIdempotentHandler.isAccomplish(id.toString())) {
                return;
            }
            throw new ServiceException("消息未完成流程,需要消息队列重试");
        }
        try {
            Map<String, String> producerMap = message.getValue();
            ShortLinkStatsRecordDTO statsRecord = JSON.parseObject(producerMap.get("statsRecord"), ShortLinkStatsRecordDTO.class);
            actualSaveShortLinkStats(statsRecord);
            stringRedisTemplate.opsForStream().delete(Objects.requireNonNull(stream), id.getValue());
        } catch (Throwable ex) {
            // 某某某情况宕机了
            messageQueueIdempotentHandler.delMessageProcessed(id.toString());
            log.error("记录短链接监控消费异常", ex);
            throw ex;
        }
        messageQueueIdempotentHandler.setAccomplish(id.toString());
    }

    public void actualSaveShortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        String fullShortUrl = statsRecord.getFullShortUrl();
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, fullShortUrl));
        RLock rLock = readWriteLock.readLock();
        rLock.lock();
        try {
            LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
            String gid = shortLinkGotoDO.getGid();
            Date currentDate = statsRecord.getCurrentDate();
            int hour = DateUtil.hour(currentDate, true);
            Week week = DateUtil.dayOfWeekEnum(currentDate);
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .uip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            Map<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", statsLocaleAmapKey);
            localeParamMap.put("ip", statsRecord.getRemoteAddr());
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infoCode = localeResultObj.getString("infocode");
            String actualProvince = "未知";
            String actualCity = "未知";
            if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .province(actualProvince = unknownFlag ? actualProvince : province)
                        .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .country("中国")
                        .date(currentDate)
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
            }
            LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                    .os(statsRecord.getOs())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);
            LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                    .browser(statsRecord.getBrowser())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(statsRecord.getDevice())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(statsRecord.getNetwork())
                    .cnt(1)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .user(statsRecord.getUv())
                    .ip(statsRecord.getRemoteAddr())
                    .browser(statsRecord.getBrowser())
                    .os(statsRecord.getOs())
                    .network(statsRecord.getNetwork())
                    .device(statsRecord.getDevice())
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .fullShortUrl(fullShortUrl)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            shortLinkMapper.incrementStats(gid, fullShortUrl, 1, statsRecord.getUvFirstFlag() ? 1 : 0, statsRecord.getUipFirstFlag() ? 1 : 0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .todayPv(1)
                    .todayUv(statsRecord.getUvFirstFlag() ? 1 : 0)
                    .todayUip(statsRecord.getUipFirstFlag() ? 1 : 0)
                    .fullShortUrl(fullShortUrl)
                    .date(currentDate)
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        } catch (Throwable ex) {
            log.error("短链接访问量统计异常", ex);
        } finally {
            rLock.unlock();
        }
    }
}

消息队列重复消费问题如何解决?

当消息队列出现重复消费问题情况下,应该如何保障数据的准确性?

  • 网络问题
  • 生产重试

如何解决?

幂等。

代码:

mq/idempotent


/**
 * 消息队列幂等处理器
 */
@Component
@RequiredArgsConstructor
public class MessageQueueIdempotentHandler {

    private final StringRedisTemplate stringRedisTemplate;

    private static final String IDEMPOTENT_KEY_PREFIX = "short-link:idempotent:";

    /**
     * 判断当前消息是否消费过
     *
     * @param messageId 消息唯一标识
     * @return 消息是否消费过
     */
    public boolean isMessageBeingConsumed(String messageId) {
        String key = IDEMPOTENT_KEY_PREFIX + messageId;
        return Boolean.FALSE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "0", 2, TimeUnit.MINUTES));
    }

    /**
     * 判断消息消费流程是否执行完成
     *
     * @param messageId 消息唯一标识
     * @return 消息是否执行完成
     */
    public boolean isAccomplish(String messageId) {
        String key = IDEMPOTENT_KEY_PREFIX + messageId;
        return Objects.equals(stringRedisTemplate.opsForValue().get(key), "1");
    }

    /**
     * 设置消息流程执行完成
     *
     * @param messageId 消息唯一标识
     */
    public void setAccomplish(String messageId) {
        String key = IDEMPOTENT_KEY_PREFIX + messageId;
        stringRedisTemplate.opsForValue().set(key, "1", 2, TimeUnit.MINUTES);
    }

    /**
     * 如果消息处理遇到异常情况,删除幂等标识
     *
     * @param messageId 消息唯一标识
     */
    public void delMessageProcessed(String messageId) {
        String key = IDEMPOTENT_KEY_PREFIX + messageId;
        stringRedisTemplate.delete(key);
    }
}

 mq/consumer中ShortLinkStatsSaveConsumer中:

常见问题

1. 如果消费者消费失败了但没有执行到删除标识,该怎么办?

(此时因为已经在isMessageBeingConsumed方法中设置了redis,所以消息失败后,应该删除redis的缓存,防止其他消息无法消费)

 比如网络宕机了,messageQueueIdempotentHandler.delMessageProcessed(id.toString())未执行,因为ack没有得到,所以mq会进行重试,会检查messageId有没有,如果有的话就会返回失败。所以在判断标识是否存在时,当已经存在标识时,还需要判断消费流程是否执行完成,防止未执行完成时,直接失败。完成等于1.

2. 为什么仅设置 2分钟的过期时间?

当生产者一直重发消息时,因为异常 redis中还有key,程序一直无法向下进行。而设计这两分钟后过期就恰好合理的解决了这个问题。

因为这个key其实是存在redis中的,如果时间过长,redis中缓存的key数据量就会大,占用内存多,所以设置时间短一些,可以减少key存储的数量。

3. 如何应对海量幂等 Key 所消耗的内存?

(因为存在redis中占用内存,所以是得不偿失的)

  • MySQL 或其它大数据量存储。(不带自动删除(自动过期),所以需要通过其它方式进行设置)
  • 想办法改造数据。(比如可以用布隆过滤器)

延迟队列:

/**
 * 延迟记录短链接统计组件
 */
@Deprecated
@Slf4j
@Component
@RequiredArgsConstructor
public class DelayShortLinkStatsConsumer implements InitializingBean {

    private final RedissonClient redissonClient;
    private final ShortLinkService shortLinkService;
    private final MessageQueueIdempotentHandler messageQueueIdempotentHandler;

    public void onMessage() {
        Executors.newSingleThreadExecutor(
                        runnable -> {
                            Thread thread = new Thread(runnable);
                            thread.setName("delay_short-link_stats_consumer");
                            thread.setDaemon(Boolean.TRUE);
                            return thread;
                        })
                .execute(() -> {
                    RBlockingDeque<ShortLinkStatsRecordDTO> blockingDeque = redissonClient.getBlockingDeque(DELAY_QUEUE_STATS_KEY);
                    RDelayedQueue<ShortLinkStatsRecordDTO> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
                    for (; ; ) {
                        try {
                            ShortLinkStatsRecordDTO statsRecord = delayedQueue.poll();
                            if (statsRecord != null) {
                                if (messageQueueIdempotentHandler.isMessageBeingConsumed(statsRecord.getKeys())) {
                                    // 判断当前的这个消息流程是否执行完成
                                    if (messageQueueIdempotentHandler.isAccomplish(statsRecord.getKeys())) {
                                        return;
                                    }
                                    throw new ServiceException("消息未完成流程,需要消息队列重试");
                                }
                                try {
                                    shortLinkService.shortLinkStats(statsRecord);
                                } catch (Throwable ex) {
                                    messageQueueIdempotentHandler.delMessageProcessed(statsRecord.getKeys());
                                    log.error("延迟记录短链接监控消费异常", ex);
                                }
                                messageQueueIdempotentHandler.setAccomplish(statsRecord.getKeys());
                                continue;
                            }
                            LockSupport.parkUntil(500);
                        } catch (Throwable ignored) {
                        }
                    }
                });
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // onMessage();
    }
}
/**
 * 延迟消费短链接统计发送者
 */
@Component
@Deprecated
@RequiredArgsConstructor
public class DelayShortLinkStatsProducer {

    private final RedissonClient redissonClient;

    /**
     * 发送延迟消费短链接统计
     *
     * @param statsRecord 短链接统计实体参数
     */
    public void send(ShortLinkStatsRecordDTO statsRecord) {
        statsRecord.setKeys(UUID.fastUUID().toString());
        RBlockingDeque<ShortLinkStatsRecordDTO> blockingDeque = redissonClient.getBlockingDeque(DELAY_QUEUE_STATS_KEY);
        RDelayedQueue<ShortLinkStatsRecordDTO> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        delayedQueue.offer(statsRecord, 5, TimeUnit.SECONDS);
    }
}

短链接Redis缓存命名重构

project:

/**
 * Redis Key 常量类
 */
public class RedisKeyConstant {

    /**
     * 短链接跳转前缀 Key
     */
    public static final String GOTO_SHORT_LINK_KEY = "short-link:goto:%s";

    /**
     * 短链接空值跳转前缀 Key
     */
    public static final String GOTO_IS_NULL_SHORT_LINK_KEY = "short-link:is-null:goto_%s";

    /**
     * 短链接跳转锁前缀 Key
     */
    public static final String LOCK_GOTO_SHORT_LINK_KEY = "short-link:lock:goto:%s";

    /**
     * 短链接修改分组 ID 锁前缀 Key
     */
    public static final String LOCK_GID_UPDATE_KEY = "short-link:lock:update-gid:%s";

    /**
     * 短链接延迟队列消费统计 Key
     */
    public static final String DELAY_QUEUE_STATS_KEY = "short-link:delay-queue:stats";

    /**
     * 短链接统计判断是否新用户缓存标识
     */
    public static final String SHORT_LINK_STATS_UV_KEY = "short-link:stats:uv:";

    /**
     * 短链接统计判断是否新 IP 缓存标识
     */
    public static final String SHORT_LINK_STATS_UIP_KEY = "short-link:stats:uip:";

    /**
     * 短链接监控消息保存队列 Topic 缓存标识
     */
    public static final String SHORT_LINK_STATS_STREAM_TOPIC_KEY = "short-link:stats-stream";

    /**
     * 短链接监控消息保存队列 Group 缓存标识
     */
    public static final String SHORT_LINK_STATS_STREAM_GROUP_KEY = "short-link:stats-stream:only-group";

    /**
     * 创建短链接锁标识
     */
    public static final String SHORT_LINK_CREATE_LOCK_KEY = "short-link:lock:create";
}

admin:

/**
 * 短链接后管 Redis 缓存常量类
 */
public class RedisCacheConstant {

    /**
     * 用户注册分布式锁
     */
    public static final String LOCK_USER_REGISTER_KEY = "short-link:lock_user-register:";

    /**
     * 分组创建分布式锁
     */
    public static final String LOCK_GROUP_CREATE_KEY = "short-link:lock_group-create:%s";

    /**
     * 用户登录缓存标识
     */
    public static final String USER_LOGIN_KEY = "short-link:login:";
}

推荐代码优雅的书:

《重构既有代码设计》、《代码整洁之道》

短链接生成重复为什么要再查询数据库?

补充:初始化Redis Stream Topic和消费组。

package com.nageoffer.shortlink.project.initialize;

/**
 * 初始化短链接监控消息队列消费者组
 */
@Component
@RequiredArgsConstructor
public class ShortLinkStatsStreamInitializeTask implements InitializingBean {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public void afterPropertiesSet() throws Exception {
        Boolean hasKey = stringRedisTemplate.hasKey(SHORT_LINK_STATS_STREAM_TOPIC_KEY);
        if (hasKey == null || !hasKey) {
            stringRedisTemplate.opsForStream().createGroup(SHORT_LINK_STATS_STREAM_TOPIC_KEY, SHORT_LINK_STATS_STREAM_GROUP_KEY);
        }
    }
}

正式:

    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成,请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            // 短链接哈希算法生成冲突问题如何解决?详情查看:https://nageoffer.com/shortlink/question
            shorUri = HashUtil.hashToBase62(originUrl);
            // 判断短链接是否存在为什么不使用Set结构?详情查看:https://nageoffer.com/shortlink/question
            // 如果布隆过滤器挂了,里边存的数据全丢失了,怎么恢复呢?详情查看:https://nageoffer.com/shortlink/question
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

异常里为什么还查询数据库?

靠什么判断短链接是否存在?布隆过滤器。

有什么特点?

  • 查询是否存在,如果返回存在,可能数据是不存在的。
  • 查询是否存在,如果返回不存在,数据一定不存在。

并发场景下会出现短链接生成重复

同一毫秒下,大量请求相同的原始链接会生成重复短链接,并判断不存在,通过该方式访问数据库。为此,我们使用 UUID 替换了当前时间戳,来一定程度减少重复的短链接生成报错。

 微服务改造

如何改造为微服务架构?

为什么要用微服务:

1. 模块化和独立性

  • 微服务:微服务架构通过将应用拆分为小型、独立的服务,每个服务专注于特定的业务功能。这种模块化的设计使得每个服务都可以独立开发、部署、扩展和维护。
  • 单体服务:在单体服务中,应用是一个大而臃肿的单一单元,修改一个功能可能会影响整个应用的部署。

2. 技术异构性

  • 微服务:允许使用不同的技术栈和编程语言来构建不同的服务,以适应不同的需求。每个微服务可以选择最适合其特定任务的技术。
  • 单体服务:通常需要在同一技术栈下构建整个应用。

3. 独立部署和扩展

  • 微服务:允许独立部署和扩展每个服务,这样可以更灵活地应对流量变化和需求变更。
  • 单体服务:需要整体部署和扩展,可能会导致资源浪费或性能瓶颈。

4. 团队自治

  • 微服务:每个微服务通常由一个小团队负责,团队可以根据其服务的需求进行独立的决策,提高了开发团队的自治性。
  • 单体服务:整个应用的变更需要协调整个团队,可能导致开发速度较慢和沟通成本较高。

5. 弹性和容错性

  • 微服务:由于每个服务都是独立的,可以更容易实现服务的弹性和容错。一个服务的故障不会影响整个应用。
  • 单体服务:一个组件的故障可能导致整个应用的崩溃。

6. 可维护性和可测试性

  • 微服务:每个微服务的小规模和清晰的职责范围使得代码更容易理解、维护和测试。
  • 单体服务:单体应用的复杂性可能导致代码难以理解,难以维护和测试。

短链接如何改造微服务:

1. 下载 Nacos

Nacos 部署:

Nacos支持三种部署模式 | Nacos 官网

2. 服务中引入 Nacos 进行服务注册

 同理,在admin中一样映入pom文件等。

引入 Pom 文件:

服务自主发起注册。

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

如果是调用方,需要引入 OpenFeign 组件。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- openfeign 已不再提供默认负载均衡器 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>

 启动类添加 Nacos 注册中心注解。

@EnableDiscoveryClient

调用方,启动类还要加入注解:

@EnableFeignClients("com.nageoffer.shortlink.admin.remote")

配置 yaml :

spring:
  application:
    name: short-link-project
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

3. 改造现有代码通过 OpenFeign 调用 

创建 OpenFeign 远程调用服务

@FeignClient("short-link-project")
public interface ShortLinkActualRemoteService {
	// 调用接口
}

feign中get请求默认是不能传对象的,如果要传对象,需要做一些配置。

还有没办法接收一个接口当泛型,要用实体。所以分页那块把IPage改成了Page

/**
 * 短链接中台远程调用服务
 */
@FeignClient(
        value = "short-link-project",
        url = "${aggregation.remote-url:}",
        configuration = OpenFeignConfiguration.class
)
public interface ShortLinkActualRemoteService {

    /**
     * 创建短链接
     *
     * @param requestParam 创建短链接请求参数
     * @return 短链接创建响应
     */
    @PostMapping("/api/short-link/v1/create")
    Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam);

    /**
     * 查询分组短链接总量
     *
     * @param requestParam 分组短链接总量请求参数
     * @return 查询分组短链接总量响应
     */
    @GetMapping("/api/short-link/v1/count")
    Result<List<ShortLinkGroupCountQueryRespDTO>> listGroupShortLinkCount(@RequestParam("requestParam") List<String> requestParam);

...
}

业务代码中引用 

private final ShortLinkActualRemoteService shortLinkActualRemoteService;

shortLinkActualRemoteService.xxx();

一般业务的 类在应用时放上面,像redisson这种中间件引用时放下面。

引入网关架构SpringCloud-Gateway

为什么需要网关:

没有网关存在的一些问题:

  • 路由管理&服务发现困难。
  • 安全性难以管理:https 访问、黑白名单、用户登录和数据请求加密放篡改等。
  • 负载均衡问题。
  • 监控和日志难以集中管理。
  • 缺乏统一的 API 管理。

没有网关:

有网关:

引入软件网关组件

更复杂的网关架构

流量网关和业务网关等。

引入 SpringCloud Gateway:
1. 引入 Pom 文件

引入 SpringCloud Gateway 相关的 Pom 组件。视频讲解中漏掉一个 build 标签,正常不会影响运行,但是打包的 Jar 文件不能运行。

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>

    <!-- Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider xxx -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
    </dependency>
</dependencies>

<build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
 2. 创建网关启动类
/**
 * 网关服务应用启动器
 */
@SpringBootApplication
public class GatewayServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServiceApplication.class, args);
    }
}
3. 添加网关配置文件
server:
  port: 8000
spring:
  application:
    name: short-link-gateway
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: short-link-admin
          uri: lb://short-link-admin/api/short-link/admin/**
          predicates:
            - Path=/api/short-link/admin/**
          filters:
            - name: TokenValidate
              args:
                whitePathList:
                  - /api/short-link/admin/v1/user/login
                  - /api/short-link/admin/v1/user/has-username

        - id: short-link-project
          uri: lb://short-link-project/api/short-link/**
          predicates:
            - Path=/api/short-link/**
          filters:
            - name: TokenValidate
4. 添加用户登录拦截器

添加白名单配置类:

package com.nageoffer.shortlink.gateway.config;

/**
 * 过滤器配置
 */
@Data
public class Config {

    /**
     * 白名单前置路径
     */
    private List<String> whitePathList;
}

 添加网关错误返回信息。

package com.nageoffer.shortlink.gateway.dto;

/**
 * 网关错误返回信息
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GatewayErrorResult {

    /**
     * HTTP 状态码
     */
    private Integer status;

    /**
     * 返回信息
     */
    private String message;
}

添加用户登录拦截器。

package com.nageoffer.shortlink.gateway.filter;

/**
 * SpringCloud Gateway Token 拦截器
 */
@Component
public class TokenValidateGatewayFilterFactory extends AbstractGatewayFilterFactory<Config> {

    private final StringRedisTemplate stringRedisTemplate;

    public TokenValidateGatewayFilterFactory(StringRedisTemplate stringRedisTemplate) {
        super(Config.class);
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String requestPath = request.getPath().toString();
            String requestMethod = request.getMethod().name();
            if (!isPathInWhiteList(requestPath, requestMethod, config.getWhitePathList())) {
                String username = request.getHeaders().getFirst("username");
                String token = request.getHeaders().getFirst("token");
                Object userInfo;
                if (StringUtils.hasText(username) && StringUtils.hasText(token) && (userInfo = stringRedisTemplate.opsForHash().get("short-link:login:" + username, token)) != null) {
                    JSONObject userInfoJsonObject = JSON.parseObject(userInfo.toString());
                    ServerHttpRequest.Builder builder = exchange.getRequest().mutate().headers(httpHeaders -> {
                        httpHeaders.set("userId", userInfoJsonObject.getString("id"));
                        httpHeaders.set("realName", URLEncoder.encode(userInfoJsonObject.getString("realName"), StandardCharsets.UTF_8));
                    });
                    return chain.filter(exchange.mutate().request(builder.build()).build());
                }
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.writeWith(Mono.fromSupplier(() -> {
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    GatewayErrorResult resultMessage = GatewayErrorResult.builder()
                            .status(HttpStatus.UNAUTHORIZED.value())
                            .message("Token validation error")
                            .build();
                    return bufferFactory.wrap(JSON.toJSONString(resultMessage).getBytes());
                }));
            }
            return chain.filter(exchange);
        };
    }

    private boolean isPathInWhiteList(String requestPath, String requestMethod, List<String> whitePathList) {
        return (!CollectionUtils.isEmpty(whitePathList) && whitePathList.stream().anyMatch(requestPath::startsWith)) || (Objects.equals(requestPath, "/api/short-link/admin/v1/user") && Objects.equals(requestMethod, "POST"));
    }
}
 5. 后管系统改造事项
5.1. 删除用户未登录错误码

因为通过 HTTP status 401 来标识用户未登录,所以需要删除后管中的自定义错误码。

package com.nageoffer.shortlink.admin.common.enums;

import com.nageoffer.shortlink.admin.common.convention.errorcode.IErrorCode;

/**
 * 用户错误码
 */
public enum UserErrorCodeEnum implements IErrorCode {

    // 需要删除
    USER_TOKEN_FAIL("A000200", "用户Token验证失败"),

    USER_NULL("B000200", "用户记录不存在"),

    USER_NAME_EXIST("B000201", "用户名已存在"),

    USER_EXIST("B000202", "用户记录已存在"),

    USER_SAVE_ERROR("B000203", "用户记录新增失败");

    private final String code;

    private final String message;

    UserErrorCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public String code() {
        return code;
    }

    @Override
    public String message() {
        return message;
    }
}
5.2. 修改用户拦截器 

将之前的操作已经迁移至网关识别,为此,该拦截器只需要保留用户上下文代码即可。

package com.nageoffer.shortlink.admin.common.biz.user;

/**
 * 用户信息传输过滤器
 */
@RequiredArgsConstructor
public class UserTransmitFilter implements Filter {

    @SneakyThrows
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String username = httpServletRequest.getHeader("username");
        if (StrUtil.isNotBlank(username)) {
            String userId = httpServletRequest.getHeader("userId");
            String realName = httpServletRequest.getHeader("realName");
            UserInfoDTO userInfoDTO = new UserInfoDTO(userId, username, realName);
            UserContext.setUser(userInfoDTO);
        }
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            UserContext.removeUser();
        }
    }
}

因为之前 Redis 操作通过构造函数创建,所以同时需要改造创建方式。

@Configuration
public class UserConfiguration {

    /**
     * 用户信息传递过滤器
     */
    @Bean
    public FilterRegistrationBean<UserTransmitFilter> globalUserTransmitFilter() {
        FilterRegistrationBean<UserTransmitFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new UserTransmitFilter());
        registration.addUrlPatterns("/*");
        registration.setOrder(0);
        return registration;
    }
}
 5.3. 修改前端代码

调整 vite.config.js 文件调用后端的端口需要从 8002 改为 8000 网关端口。

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        ws: true,
        rewrite: (path) => path.replace(/^\/api/, '') // 不可以省略rewrit
      }
    }
  }
})

调整 axios.js 文件的用户未登录跳转方式,之前通过 res.data.code === 'A000200' 判断,现在通过 err.response.status === 401 判断。

import axios from 'axios'
import { getToken, getUsername } from '@/core/auth.js'
// import Router from '../router'
import { ElMessage } from 'element-plus'
import { isNotEmpty } from '@/utils/plugins.js'
import { useRouter } from 'vue-router'
const router = useRouter()
// const baseURL = '/resourcesharing/organizational'
const baseURL = '/api/short-link/admin/v1'
// 创建实例
const http = axios.create({
  // api 代理为服务器请求地址
  baseURL: '/api' + baseURL,
  timeout: 15000
})
// 请求拦截 -->在请求发送之前做一些事情
http.interceptors.request.use(
  (config) => {
    config.headers.Token = isNotEmpty(getToken()) ? getToken() : ''
    config.headers.Username = isNotEmpty(getUsername()) ? getUsername() : ''
    // console.log('获取到的token和username', getToken(), getUsername())
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)
// 响应拦截 -->在返回结果之前做一些事情
http.interceptors.response.use(
  (res) => {
    if (res.status == 0 || res.status == 200) {
      // 请求成功对响应数据做处理,此处返回的数据是axios.then(res)中接收的数据
      // code值为 0 或 200 时视为成功
      return Promise.resolve(res)
    }
    return Promise.reject(res)
  },
  (err) => {
    // 在请求错误时要做的事儿
    // 此处返回的数据是axios.catch(err)中接收的数据
    if (err.response.status === 401) {
      localStorage.removeItem('token')
      router.push('/login')
    }
    return Promise.reject(err)
  }
)
export default http
 补充:@SneakyThrows的作用

@SneakyThrows是Lombok库提供的一个注解,其作用主要用于处理Java中的受检异常。

  • 自动转换异常:在方法上使用@SneakyThrows注解后,该方法中抛出的所有检查型异常(checked exceptions)会被自动转换为非检查型异常(unchecked exceptions),即java.lang.RuntimeException或其子类。这样,开发者就无需在方法签名中声明这些检查型异常,也无需在方法体内显式地编写try-catch语句来处理它们。
  • 简化代码:通过自动转换异常,@SneakyThrows注解可以显著减少代码量,使代码更加简洁。它避免了在方法签名中声明大量检查型异常,并减少了方法体内的异常处理代码。
 引入网关架构后如何访问中台?

后管作为可视化界面方式操作短链接系统,中台作为提供后管接口调用以及 API 等多种调用方式。

此时,中台应用就需要进行独立的用户登录验证逻辑。

密钥方式

在用户记录生成时创建唯一的密钥进行保存,每次访问时都携带该密钥访问即可。

和后管沿用一套方案

和当前后管服务沿用一套登录机制,每次都带上用户的登录 Token 访问中台接口即可。

Q:如果用户在后管中操作了退出登录如何解决?

A:应该在客户端应用调用后,发现请求返回的 401,重新调用登录接口,再发起一次调用即可。

Q:如果用户登录状态失效,会请求 401 如何解决?

A:改造登录接口,如果用户已登录情况,那么重新刷新有效期。

开发短链接聚合服务

微服务中的聚合服务,顾名思义,是指将多个相关的微服务或服务功能聚集在一起,形成一个更高级别、更综合的服务单元。这种服务模式在微服务架构中尤为重要,因为它有助于优化服务间的协同工作,减少服务间的通信成本,提升系统的整体性能和响应速度。

创建聚合服务

创建聚合服务 Modules(aggregation),并创建对应的启动类和 Pom.xml。

1. 聚合服务启动类
package com.nageoffer.shortlink.aggregation;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * 短链接聚合应用
 */
@SpringBootApplication(scanBasePackages = {
        "com.nageoffer.shortlink.admin",
        "com.nageoffer.shortlink.project"
})
@EnableDiscoveryClient
@MapperScan(value = {
        "com.nageoffer.shortlink.project.dao.mapper",
        "com.nageoffer.shortlink.admin.dao.mapper"
})
public class AggregationServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AggregationServiceApplication.class, args);
    }
}
2. 聚合服务 Pom.xml 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.nageoffer.shortlink</groupId>
        <artifactId>shortlink-all</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>shortlink-aggregation</artifactId>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>shortlink-admin</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>shortlink-project</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
创建聚合服务应用配置 
1. application.yaml
server:
  port: 8003

spring:
  application:
    name: short-link-aggregation
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere-config-${database.env:dev}.yaml
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
  mvc:
    view:
      prefix: /templates/
      suffix: .html
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

short-link:
  group:
    max-num: 20
  flow-limit:
    enable: true
    time-window: 1
    max-access-count: 20
  domain:
    default: nurl.ink:8003
  stats:
    locale:
      amap-key: 824c511f0997586ea016f979fdb23087
  goto-domain:
    white-list:
      enable: true
      names: '拿个offer,知乎,掘金,博客园'
      details:
        - nageoffer.com
        - zhihu.com
        - juejin.cn
        - cnblogs.com

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
2. shardingsphere-config-dev.yaml
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

rules:
  - !SHARDING
    tables:
      t_user:
        actualDataNodes: ds_0.t_user_${0..15}
        tableStrategy:
          standard:
            shardingColumn: username
            shardingAlgorithmName: user_table_hash_mod
      t_group:
        actualDataNodes: ds_0.t_group_${0..15}
        tableStrategy:
          standard:
            shardingColumn: username
            shardingAlgorithmName: group_table_hash_mod
      t_link:
        actualDataNodes: ds_0.t_link_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_table_hash_mod
      t_link_goto:
        actualDataNodes: ds_0.t_link_goto_${0..15}
        tableStrategy:
          standard:
            shardingColumn: full_short_url
            shardingAlgorithmName: link_goto_table_hash_mod
      t_link_stats_today:
        actualDataNodes: ds_0.t_link_stats_today_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_stats_today_hash_mod
    bindingTables:
      - t_link, t_link_stats_today
    shardingAlgorithms:
      user_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      group_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_goto_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_stats_today_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
  - !ENCRYPT
    tables:
      t_user:
        columns:
          phone:
            cipherColumn: phone
            encryptorName: common_encryptor
          mail:
            cipherColumn: mail
            encryptorName: common_encryptor
        queryWithCipherColumn: true
    encryptors:
      common_encryptor:
        type: AES
        props:
          aes-key-value: d6oadClrrb9A3GWo
props:
  sql-show: true
3. shardingsphere-config-prod.yaml 
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: PHRmUcd6ZpM0506N1wldC9EKsix77VA8HwMHloLJPZtxSkMnRfEKSn8SYpvcaI5

rules:
  - !SHARDING
    tables:
      t_user:
        actualDataNodes: ds_0.t_user_${0..15}
        tableStrategy:
          standard:
            shardingColumn: username
            shardingAlgorithmName: user_table_hash_mod
      t_group:
        actualDataNodes: ds_0.t_group_${0..15}
        tableStrategy:
          standard:
            shardingColumn: username
            shardingAlgorithmName: group_table_hash_mod
      t_link:
        actualDataNodes: ds_0.t_link_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_table_hash_mod
      t_link_goto:
        actualDataNodes: ds_0.t_link_goto_${0..15}
        tableStrategy:
          standard:
            shardingColumn: full_short_url
            shardingAlgorithmName: link_goto_table_hash_mod
      t_link_stats_today:
        actualDataNodes: ds_0.t_link_stats_today_${0..15}
        tableStrategy:
          standard:
            shardingColumn: gid
            shardingAlgorithmName: link_stats_today_hash_mod
    bindingTables:
      - t_link, t_link_stats_today
    shardingAlgorithms:
      user_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      group_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_goto_table_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
      link_stats_today_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 16
  - !ENCRYPT
    tables:
      t_user:
        columns:
          phone:
            cipherColumn: phone
            encryptorName: common_encryptor
          mail:
            cipherColumn: mail
            encryptorName: common_encryptor
        queryWithCipherColumn: true
    encryptors:
      common_encryptor:
        type: AES
        props:
          aes-key-value: d6oadClrrb9A3GWo
props:
  sql-show: true
改造后管服务调用中台的方式 
1. 添加配置
aggregation:
	remote-url: http://127.0.0.1:${server.port}
2. FeignClient 改造 
@FeignClient(value = "short-link-project", url = "${aggregation.remote-url:}")
配置网关访问聚合服务
1. application.yaml
server:
  port: 8000
spring:
  application:
    name: short-link-gateway
  profiles:
    active: aggregation
    # active: dev
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
2. application-dev.yaml
spring:
  cloud:
    gateway:
      routes:
        - id: short-link-admin
          uri: lb://short-link-admin/api/short-link/admin/**
          predicates:
            - Path=/api/short-link/admin/**
          filters:
            - name: TokenValidate
              args:
                whitePathList:
                  - /api/short-link/admin/v1/user/login
                  - /api/short-link/admin/v1/user/has-username

        - id: short-link-project
          uri: lb://short-link-project/api/short-link/**
          predicates:
            - Path=/api/short-link/**
          filters:
            - name: TokenValidate
3. application-aggregation.yaml
spring:
  cloud:
    gateway:
      routes:
        - id: short-link-admin-aggregation
          uri: lb://short-link-aggregation/api/short-link/admin/**
          predicates:
            - Path=/api/short-link/admin/**
          filters:
            - name: TokenValidate
              args:
                whitePathList:
                  - /api/short-link/admin/v1/user/login
                  - /api/short-link/admin/v1/user/has-username

        - id: short-link-project-aggregation
          uri: lb://short-link-aggregation/api/short-link/**
          predicates:
            - Path=/api/short-link/**
          filters:
            - name: TokenValidate

聚合服务中:

由于project和admin有很多相同的bean,所以需要在project和admin中对bean加名字。

@ConditionalOnMissingBean条件注解,检查ioc里面有没有这个bean,有的话就不注入了。

@Primary注解的作用:

标记首选Bean:@Primary注解用于标记一个Bean作为在多个同类型的Bean候选中进行自动装配时的首选Bean。当一个接口有多个实现类,或者多个Bean属于同一类型时,使用@Primary注解可以明确指定哪一个Bean应该被优先考虑。这样,在注入该类型的Bean时,Spring容器会优先选择带有@Primary注解的Bean进行注入。 

 解决自动装配冲突:在Spring容器中,如果存在多个相同类型的Bean,而自动装配时又未明确指定具体哪一个Bean,那么就会出现自动装配冲突的问题。使用@Primary注解可以明确指定哪一个Bean应该被优先考虑,从而避免这种冲突,确保注入过程的顺利进行。

简化配置:通过@Primary注解,开发人员可以避免在每个注入点使用@Qualifier注解来指定具体的Bean名称,从而简化了代码和配置。这不仅使得代码更加简洁,也提高了代码的可读性和可维护性。

使用场景

  • 当一个接口有多个实现类时,可以使用@Primary注解来指定其中一个实现类作为默认的候选项。
  • 在配置和自动装配复杂的Spring应用程序时,特别是当有多个Bean实现相同的接口或继承相同的类时,@Primary注解非常有用。

线上环境部署短链接服务(聚合服务)

使用java -jar这种形式启动项目,和idea启动是不一样的,一个是用的tomcat的类加载器,一个是用的idea的类加载器。

使用聚合模式时,需要把admin和project中的builder删除,只留聚合服务中的builder。

如何通过域名访问线上服务

相关推荐

  1. day3

    2024-07-18 01:56:03       24 阅读
  2. 的理解

    2024-07-18 01:56:03       33 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 01:56:03       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 01:56:03       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 01:56:03       58 阅读
  4. Python语言-面向对象

    2024-07-18 01:56:03       69 阅读

热门阅读

  1. 【SASS/SCSS(二)】模块化语法

    2024-07-18 01:56:03       26 阅读
  2. HTML5应用的安全防护策略与实践

    2024-07-18 01:56:03       22 阅读
  3. 23种设计模式

    2024-07-18 01:56:03       20 阅读
  4. tomcat如何进行调优?

    2024-07-18 01:56:03       16 阅读
  5. C#调用非托管dll的两种方式

    2024-07-18 01:56:03       21 阅读
  6. WEB渗透之相关概念(笔记)

    2024-07-18 01:56:03       22 阅读
  7. idea 运行异常 gradle 项目

    2024-07-18 01:56:03       20 阅读
  8. C++ Primer:3.6 多维数组

    2024-07-18 01:56:03       24 阅读
  9. 设计模式大白话之适配器模式

    2024-07-18 01:56:03       24 阅读