Loading

冗余双写思路总结

新增全流程

创建全流程

Request

{
    "groupId":"825487726962655232",
    "title":"zzzzzz全链路测试",
    "originalUrl":"https://ccl.zone?asdf=a21",
    "domainId":"1613160674939047938",
    "domainType":"CUSTOM",
    "expired":"-1"
}

Controller

@PostMapping("/add")
public JsonData createShortLink(@RequestBody ShortLinkAddRequest request){

    JsonData jsonData = shortLinkService.createShortLink(request);
    return jsonData;
    
}

Service

Producer

@Override
public JsonData createShortLink(ShortLinkAddRequest request) {

    Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

    String newOriginalUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
    request.setOriginalUrl(newOriginalUrl);

    EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
        .content(JsonUtil.obj2Json(request))
        .messageId(IDUtil.geneSnowFlakeID().toString())
        .eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
        .build();

    rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkAddRoutingKey(), eventMessage);

    return JsonData.buildSuccess();
}

Consumer

Lua + Redis 释义

//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" + " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" + " else return 0; end;";

Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);

* 先判断是否有,如没这个key,则设置key-value,配置过期时间,加锁成功
    
    if redis.call('EXISTS',KEYS[1]*shortLinkCode*)==0 then redis.call('set',KEYS[1]*shortLinkCode*,ARGV[1]*accountNo*);
	redis.call('expire',KEYS[1]*shortLinkCode*,ARGV[2]*100*);
    
* 如果有这个key,判断value是否是同个账号,是同个账号则返回加锁成功
    
    elseif redis.call('get',KEYS[1])*shortLinkCode* == ARGV[1]*accountNo* then return 2
    
* 如果不是同个账号则加锁失败
     else return 0; end;

image-20230223205541415

    /**
     * 处理短链新增逻辑
     * <p>
     * //判断短链域名是否合法
     * //判断组名是否合法
     * //生成长链摘要
     * //生成短链码
     * //加锁
     * //查询短链码是否存在
     * //构建短链对象
     * //保存数据库
     *
     * @param eventMessage
     * @return
     */
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {

    Long accountNo = eventMessage.getAccountNo();
    String messageType = eventMessage.getEventMessageType();

    ShortLinkAddRequest addRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
    //短链域名校验
    DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);
    //校验组是否合法
    LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);

    //长链摘要
    String originalUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());

    //短链码重复标记
    boolean duplicateCodeFlag = false;

    //生成短链码
    String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());

    //加锁
    //key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
    String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
        " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
        " else return 0; end;";

    Long result = redisTemplate.execute(new
                                        DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);

    //加锁成功
    if (result > 0) {

        //C端处理
        if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {

            //先判断是否短链码被占用
            ShortLinkDO shortLinCodeDOInDB = shortLinkManager.findByShortLinCode(shortLinkCode);

            if (shortLinCodeDOInDB == null) {
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .accountNo(accountNo).code(shortLinkCode)
                    .title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl())
                    .domain(domainDO.getValue()).groupId(linkGroupDO.getId())
                    .expired(addRequest.getExpired()).sign(originalUrlDigest)
                    .state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
                shortLinkManager.addShortLink(shortLinkDO);
                return true;
            } else {
                log.error("C端短链码重复:{}", eventMessage);
                duplicateCodeFlag = true;
            }

        } else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
            //B端处理
            GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);

            if (groupCodeMappingDOInDB == null) {

                GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
                    .accountNo(accountNo).code(shortLinkCode).title(addRequest.getTitle())
                    .originalUrl(addRequest.getOriginalUrl())
                    .domain(domainDO.getValue()).groupId(linkGroupDO.getId())
                    .expired(addRequest.getExpired()).sign(originalUrlDigest)
                    .state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();

                groupCodeMappingManager.add(groupCodeMappingDO);
                return true;

            } else {
                log.error("B端短链码重复:{}", eventMessage);
                duplicateCodeFlag = true;
            }
        }
    } else {

        //加锁失败,自旋100毫秒,再调用; 失败的可能是短链码已经被占用,需要重新生成
        log.error("加锁失败:{}", eventMessage);

        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
        }

        duplicateCodeFlag = true;

    }

    if (duplicateCodeFlag) {
        String newOriginalUrl = CommonUtil.addUrlPrefixVersion(addRequest.getOriginalUrl());
        addRequest.setOriginalUrl(newOriginalUrl);
        eventMessage.setContent(JsonUtil.obj2Json(addRequest));
        log.warn("短链码报错失败,重新生成:{}", eventMessage);
        handlerAddShortLink(eventMessage);
    }
    return false;
}

Util

    /**
     * 校验域名
     *
     * @param domainType
     * @param domainId
     * @param accountNo
     * @return
     */
    private DomainDO checkDomain(String domainType, Long domainId, Long accountNo) {

        DomainDO domainDO;

        if (DomainTypeEnum.CUSTOM.name().equalsIgnoreCase(domainType)) {
            domainDO = domainManager.findById(domainId, accountNo);

        } else {
            domainDO = domainManager.findByDomainTypeAndID(domainId, DomainTypeEnum.OFFICIAL);
        }
        Assert.notNull(domainDO, "短链域名不合法");
        return domainDO;
    }

    /**
     * URL增加前缀
     * @param url
     * @return
     */
    public static String addUrlPrefix(String url){

        return IDUtil.geneSnowFlakeID()+"&"+url;

    }

    /**
     * 生成短链码
     * @param param
     * @return
     */
    public String createShortLinkCode(String param){

        long murmurhash = CommonUtil.murmurHash32(param);
        //进制转换
        String code = encodeToBase62(murmurhash);
        String shortLinkCode = ShardingDBConfig.getRandomDBPrefix(code) + code + ShardingTableConfig.getRandomTableSuffix(code);
        return shortLinkCode;
    }

    /**
     * 校验组名
     *
     * @param groupId
     * @param accountNo
     * @return
     */
    private LinkGroupDO checkLinkGroup(Long groupId, Long accountNo) {

        LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
        Assert.notNull(linkGroupDO, "组名不合法");
        return linkGroupDO;
    }

    /**
     * MD5加密
     *
     * @param data
     * @return
     */
    public static String MD5(String data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }

            return sb.toString().toUpperCase();
        } catch (Exception exception) {
        }
        return null;

    }

RabbitMQ

Listener

@Component
@Slf4j
//@RabbitListener(queues = "short_link.add.link.queue")
@RabbitListener(queuesToDeclare = {@Queue("short_link.add.link.queue")})
public class ShortLinkAddLinkMQListener {


    @Autowired
    private ShortLinkService shortLinkService;

    @RabbitHandler
    public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
        log.info("监听到消息ShortLinkAddLinkMQListener message消息内容:{}", message);
        try {

            eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_LINK.name());
            shortLinkService.handlerAddShortLink(eventMessage);

        } catch (Exception e) {

            //处理业务异常,还有进行其他操作,比如记录失败原因
            log.error("消费失败:{}", eventMessage);
            throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
        }
        log.info("消费成功:{}", eventMessage);
        //确认消息消费成功
//        channel.basicAck(tag, false);

    }

}

Mapping

@Component
@Slf4j
//@RabbitListener(queues = "short_link.add.mapping.queue")
@RabbitListener(queuesToDeclare = {@Queue("short_link.add.mapping.queue")})
public class ShortLinkAddMappingMQListener {


    @Autowired
    private ShortLinkService shortLinkService;

    @RabbitHandler
    public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
        log.info("监听到消息ShortLinkAddMappingMQListener message消息内容:{}", message);
        try {

            eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_MAPPING.name());
            shortLinkService.handlerAddShortLink(eventMessage);

        } catch (Exception e) {

            //处理业务异常,还有进行其他操作,比如记录失败原因
            log.error("消费失败:{}", eventMessage);
            throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
        }
        log.info("消费成功:{}", eventMessage);
        //确认消息消费成功
//        channel.basicAck(tag, false);

    }
}

Config

    /**
     * 交换机
     */
    private String shortLinkEventExchange="short_link.event.exchange";

    /**
     * 创建交换机 Topic类型
     * 一般一个微服务一个交换机
     * @return
     */
    @Bean
    public Exchange shortLinkEventExchange(){

        return new TopicExchange(shortLinkEventExchange,true,false);
    }

    //新增短链相关配置====================================

    /**
     * 新增短链 队列
     */
    private String shortLinkAddLinkQueue="short_link.add.link.queue";

    /**
     * 新增短链映射 队列
     */
    private String shortLinkAddMappingQueue="short_link.add.mapping.queue";

    /**
     * 新增短链具体的routingKey,【发送消息使用】
     */
    private String shortLinkAddRoutingKey="short_link.add.link.mapping.routing.key";

    /**
     * topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
     */
    private String shortLinkAddLinkBindingKey="short_link.add.link.*.routing.key";

    /**
     * topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
     */
    private String shortLinkAddMappingBindingKey="short_link.add.*.mapping.routing.key";


    /**
     * 新增短链api队列和交换机的绑定关系建立
     */
    @Bean
    public Binding shortLinkAddApiBinding(){
        return new Binding(shortLinkAddLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddLinkBindingKey,null);
    }


    /**
     * 新增短链mapping队列和交换机的绑定关系建立
     */
    @Bean
    public Binding shortLinkAddMappingBinding(){
        return new Binding(shortLinkAddMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddMappingBindingKey,null);
    }


    /**
     * 新增短链api 普通队列,用于被监听
     */
    @Bean
    public Queue shortLinkAddLinkQueue(){
        return new Queue(shortLinkAddLinkQueue,true,false,false);
    }

    /**
     * 新增短链mapping 普通队列,用于被监听
     */
    @Bean
    public Queue shortLinkAddMappingQueue(){
        return new Queue(shortLinkAddMappingQueue,true,false,false);
    }

DBConfig

public class ShardingDBConfig {

    /**
     * 存储数据库位置编号
     */
    private static final List<String> dbPrefixList = new ArrayList<>();

    //配置启用那些库的前缀
    static {
        dbPrefixList.add("0");
        dbPrefixList.add("1");
        dbPrefixList.add("a");
    }

    /**
     * 获取随机的前缀
     *
     * @return
     */
    public static String getRandomDBPrefix(String code) {

        int hashCode = code.hashCode();
        int index = Math.abs(hashCode) % dbPrefixList.size();
        return dbPrefixList.get(index);
    }
}

public class ShardingTableConfig {

    /**
     * 存储数据表位置编号
     */
    private static final List<String> tableSuffixList = new ArrayList<>();

    //配置启用那些表的后缀
    static {
        tableSuffixList.add("0");
        tableSuffixList.add("a");
    }


    /**
     * 获取随机的后缀
     *
     * @return
     */
    public static String getRandomTableSuffix(String code) {

        int hashCode = code.hashCode();
        int index = Math.abs(hashCode) % tableSuffixList.size();
        return tableSuffixList.get(index);
    }
}

POJO

public class EventMessage implements Serializable {

    /**
     * 消息队列的消息id
     */
    private String messageId;


    /**
     * 事件类型
     */
    private String eventMessageType;


    /**
     * 业务id
     */
    private String bizId;


    /**
     * 账号
     */
    private Long accountNo;


    /**
     * 消息体
     */
    private String content;

    /**
     * 备注
     */
    private String remark;

}

更新全流程

更新全流程

Controoler

/**
* 更新短链
* @param request
* @return
*/
@PostMapping("update")
public JsonData update(@RequestBody ShortLinkUpdateRequest request){

    JsonData jsonData = shortLinkService.update(request);

    return jsonData;
}

Service

Producer

@Override
public JsonData update(ShortLinkUpdateRequest request) {
    Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();


    EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
        .content(JsonUtil.obj2Json(request))
        .messageId(IDUtil.geneSnowFlakeID().toString())
        .eventMessageType(EventMessageType.SHORT_LINK_UPDATE.name())
        .build();

    rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(),
                                  rabbitMQConfig.getShortLinkUpdateRoutingKey(), eventMessage);

    return JsonData.buildSuccess();
}

Consumer

@Override
public boolean handlerUpdateShortLink(EventMessage eventMessage) {

    Long accountNo = eventMessage.getAccountNo();

    String messageType = eventMessage.getEventMessageType();

    ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);

    //短链域名校验
    DomainDO domainDO = checkDomain(request.getDomainType(), request.getDomainId(), accountNo);

    if (EventMessageType.SHORT_LINK_UPDATE_LINK.name().equalsIgnoreCase(messageType)) {

        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
            .accountNo(accountNo).code(request.getCode()).title(request.getTitle())
            .domain(domainDO.getValue()).build();

        int rows = shortLinkManager.update(shortLinkDO);
        log.debug("更新C端短链,row:{}", rows);
        return true;

    } else if (EventMessageType.SHORT_LINK_UPDATE_MAPPING.name().equalsIgnoreCase(messageType)) {

        GroupCodeMappingDO mappingDO = GroupCodeMappingDO.builder().accountNo(accountNo).code(request.getCode()).title(request.getTitle())
            .domain(domainDO.getDomainType()).build();

        int rows = groupCodeMappingManager.update(mappingDO);
        log.debug("更新B端短链,row:{}", rows);
        return true;

    }

    return false;
}

RabbitMQ

Listener

@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("short_link.update.link.queue")})
public class ShortLinkUpdateLinkMQListener {

    @Autowired
    private ShortLinkService shortLinkService;

    @RabbitHandler
    public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) {

        log.info("监听到消息");
        try {

            eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_LINK.name());
            shortLinkService.handlerUpdateShortLink(eventMessage);

        } catch (Exception e) {
            log.error("消费失败:{}", eventMessage);
            throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
        }
        log.info("消费成功:{}", eventMessage);
    }
}

Mapping

@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("short_link.update.mapping.queue")})
public class ShortLinkUpdateMappingMQListener {

    @Autowired
    private ShortLinkService shortLinkService;

    @RabbitHandler
    public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) {

        log.info("监听消息:{}", message);

        try {

            eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name());
            shortLinkService.handlerUpdateShortLink(eventMessage);

        } catch (Exception e) {
            log.info("消费失败:{}", eventMessage);
            throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
        }
        log.info("消费成功:{}", eventMessage);
    }
}

Config

	//更新短链相关配置====================================

    /**
     * 更新短链 队列
     */
    private String shortLinkUpdateLinkQueue="short_link.update.link.queue";

    /**
     * 更新短链映射 队列
     */
    private String shortLinkUpdateMappingQueue="short_link.update.mapping.queue";

    /**
     * 更新 短链具体的routingKey,【发送消息使用】
     */
    private String shortLinkUpdateRoutingKey="short_link.update.link.mapping.routing.key";

    /**
     * topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
     */
    private String shortLinkUpdateLinkBindingKey="short_link.update.link.*.routing.key";

    /**
     * topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
     */
    private String shortLinkUpdateMappingBindingKey="short_link.update.*.mapping.routing.key";

    /**
     * 更新操作  短链api队列 和交换机的绑定关系建立
     */
    @Bean
    public Binding shortLinkUpdateApiBinding(){
        return new Binding(shortLinkUpdateLinkQueue,Binding.DestinationType.QUEUE,
                shortLinkEventExchange,shortLinkUpdateLinkBindingKey,null);
    }

    /**
     * 更新操作 短链mapping队列和交换机的绑定关系建立
     */
    @Bean
    public Binding shortLinkUpdateMappingBinding(){
        return new Binding(shortLinkUpdateMappingQueue,Binding.DestinationType.QUEUE,
                shortLinkEventExchange,shortLinkUpdateMappingBindingKey,null);
    }

    /**
     * 更新操作 短链api 普通队列,用于被监听
     */
    @Bean
    public Queue shortLinkUpdateLinkQueue(){
        return new Queue(shortLinkUpdateLinkQueue,true,false,false);
    }

    /**
     * 更新操作 短链mapping 普通队列,用于被监听
     */
    @Bean
    public Queue shortLinkUpdateMappingQueue(){
        return new Queue(shortLinkUpdateMappingQueue,true,false,false);
    }

删除全流程

删除流程与更新类同,注意逻辑删除字段

读取全流程

读取全流程

302与301

Http-Headers-Location

controller

shortLinkCode:02BWri40

    /**
     * 解析 301还是302,这边是返回http code是302
     * <p>
     * 知识点一,为什么要用 301 跳转而不是 302 呐?
     * <p>
     * 301 是永久重定向,302 是临时重定向。
     * <p>
     * 短地址一经生成就不会变化,所以用 301 是同时对服务器压力也会有一定减少
     * <p>
     * 但是如果使用了 301,无法统计到短地址被点击的次数。
     * <p>
     * 所以选择302虽然会增加服务器压力,但是有很多数据可以获取进行分析
     *
     * @param linkCode
     * @return
     */
    @GetMapping(path = "/{shortLinkCode}")
    public void dispatch(@PathVariable(name = "shortLinkCode") String shortLinkCode,
                         HttpServletRequest request, HttpServletResponse response) {

        try {
            log.info("短链码:{}", shortLinkCode);
            //判断短链码是否合规
            if (isLetterDigit(shortLinkCode)) {
                //查找短链
                ShortLinkVO shortLinkVO = shortLinkService.parseShortLinkCode(shortLinkCode);
                //判断是否过期和可用
                if (isVisitable(shortLinkVO)) {
                    String originalUrl = CommonUtil.removeUrlPrefix(shortLinkVO.getOriginalUrl());
                    response.setHeader("Location", originalUrl);
                    //302跳转
                    response.setStatus(HttpStatus.FOUND.value());
                } else {
                    response.setStatus(HttpStatus.NOT_FOUND.value());
                    return;
                }
            }
        } catch (Exception e) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }

Util

    /**
     * 移除URL前缀
     * @param url
     * @return
     */
    public static String removeUrlPrefix(String url){
        String originalUrl = url.substring(url.indexOf("&")+1);
        return originalUrl;
    }

    /**
     * 仅包括数字和字母
     *
     * @param str
     * @return
     */
    private static boolean isLetterDigit(String str) {
        String regex = "^[a-z0-9A-Z]+$";
        return str.matches(regex);
    }

    /**
     * 判断短链是否可用
     *
     * @param shortLinkVO
     * @return
     */
    private static boolean isVisitable(ShortLinkVO shortLinkVO) {
        if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() > CommonUtil.getCurrentTimestamp())) {
            if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
                return true;
            }
        } else if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() == -1)) {
            if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
                return true;
            }
        }
        return false;
    }
posted @ 2023-02-23 22:57  yonugleesin  阅读(225)  评论(0编辑  收藏  举报