亿级流量短链接地址服务企业级实现方案

1.布景

在咱们日常生活中,短链接地址随处可见,首要可分为两大场景:短信服务发送短信和运营模版消息推送,一般都会带着链接地址。有时分链接地址很长,如:https://blog.csdn.net/weixin_44763987/article/details/106638461?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-106638461-blog-120192233.pc_relevant_antiscanv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-106638461-blog-120192233.pc_relevant_antiscanv3&utm_relevant_index=9,这样长的 URL 明显体验并不友爱(ps:短信是按字数收费的哦)。咱们希望分享的是一些更短、更易于阅览的短 URL,比方像 1.cn/23sM5J 这样的。当用户点击这个短 URL 的时分,能够重定向拜访到原始的链接地址。为此咱们将规划开发一个短 URL 生成器。

项目引荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层结构封装,解决事务开发时常见的非功用性需求,防止重复造轮子,便利事务快速开发和企业技术栈结构一致办理。引进组件化的思维实现高内聚低耦合并且高度可装备化,做到可插拔。严格控制包依靠和一致版本办理,做到最少化依靠。注重代码规范和注释,十分适合个人学习和企业运用

Github地址:github.com/plasticene/…

Gitee地址:gitee.com/plasticene3…

微信公众号Shepherd进阶笔记

2.概要规划

短链接服务中心便是构建短链接和长链接的仅有映射关系,当浏览器经过短 URL 生成器拜访这个短 URL 的时分,重定向拜访到原始的长 URL 方针服务器。

2.1运用示例

展现一个短链接在日常短信中的运用示例:这是我收到丝芙兰的会员积分兑换短信(ps:买化妆品能够找我,内部价,里边​有对象),

亿级流量短链接地址服务企业级实现方案

从图中能够看出短信内容里边有一个短链接ew7.cn/?M2Fj,点击链接你会发现发现客户端会重定向到长链接https://m.sephora.cn/v2/html/rewardsBoutique/,然后再跳转到登录页,登录成功之后就会进入长链接地址页面积分兑换。

2.2 紧缩码生成规划

短 URL 选用 Base64 编码,假如短 URL 长度是 7 个字符的话,大约能够编码 4 万亿个短 URL。64^7≈4万亿。

假如短 URL 长度是 6 个字符的话,大约能够编码 680 亿个短 URL。64^6≈680亿

依照事务量评估,6 个字符的编码一般情况就能够满意需求。因此咱们这儿的短 URL 编码长度 6 个字符,形如 1.cn/S3ke6J

单项散列函数生成紧缩码

通常的规划计划是,将长 URL 利用 MD5 或许 SHA256 等单项散列算法,进行 Hash 核算,得到 128bit 或许 256bit 的 Hash 值。然后对该 Hash 值进行 Base64 编码,得到 22 个或许 43 个 Base64 字符,再截取前面的 6 个字符,就得到短 URL 了,可是这样得到的短 URL,或许会发生 Hash 抵触,即不同的长 URL,核算得到的短 URL 是相同的(MD5 或许 SHA256 核算得到的 Hash 值简直不会抵触,可是 Base64 编码后再切断的 6 个字符有或许会抵触)。所以在生成的时分,需求先校验该短 URL 是否现已映射为其他的长 URL,假如是,那么需求从头核算(换单向散列算法,或许换 Base64 编码切断位置)。从头核算得到的短 URL 依然或许抵触,需求再从头核算。可是这样的抵触处理需求多次到存储中查找 URL,无法保证功能要求。

自增加id生成紧缩码

一种免抵触的算法是用自增加自然数来实现,即维持一个自增加的二进制自然数,能够是数据库自增主键,或许分布式id,然后将该自然数进行 Base64 编码即可得到一系列的短 URL。这样生成的的短 URL 必然仅有,并且还能够生成小于 6 个字符的短 URL,比方自然数 0 的 Base64 编码是字符“A”,就能够用 1.cn/A 作为短 URL。可是这种算法将导致短 URL 是可猜测的,假如某个运用在某个时刻段内生成了一批短 URL,那么这批短 URL 就会会集在一个自然数区间内。只要知道了其中一个短 URL,就能够经过自增(以及自减)的方式请求拜访其他 URL。

预生成紧缩码

因此,咱们选用预生成紧缩码 的计划。即预先生成一批没有抵触的短 URL 字符串,当外部请求输入长 URL 需求生成短 URL 的时分,直接从预先生成好的短 URL 字符串池中获取一个即可。预生成短 URL 的算法能够选用随机数来实现,6 个字符,每个字符都用随机数发生(用 0~63 的随机数发生一个 Base64 编码字符)。为了防止随机数发生的短 URL 抵触,需求在预生成的时分检查该 URL 是否现已存在(用布隆过滤器检查),这些操作都是在生产短链接之前就完成的,所以不会影响生成短链接的功能。

2.3 生成短链接流程

短链接服务事务逻辑比较简单,相对比较有挑战的便是高并发的读请求怎么处理、预生成的短 URL 怎么存储以及拜访。高并发拜访首要经过负载均衡与分布式缓存解决,生成短链接和短链接重定向两个接口一般QPS极高,咱们需求进步这两个接口的功能,系统架构图如下

亿级流量短链接地址服务企业级实现方案

3.实现

3.1 数据库规划

-- ----------------------------
-- Table structure for unique_code
-- ----------------------------
DROP TABLE IF EXISTS `unique_code`;
CREATE TABLE `unique_code` (
 `id` bigint(20) NOT NULL,
 `code` varchar(16) NOT NULL COMMENT '紧缩码',
 `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态 :0:未运用  1:已运用  -1:失效',
 `type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '生成方式:0:随机数   1:分布式id  2:hash',
 `deleted` tinyint(4) NOT NULL DEFAULT '0',
 `creator` bigint(20) DEFAULT NULL,
 `updater` bigint(20) DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='紧缩码表';
​
-- ----------------------------
-- Table structure for url_link
-- ----------------------------
DROP TABLE IF EXISTS `url_link`;
CREATE TABLE `url_link` (
 `id` bigint(20) NOT NULL,
 `unique_code` varchar(255) NOT NULL COMMENT '仅有紧缩码',
 `short_url` varchar(255) NOT NULL COMMENT '短链接地址',
 `long_url` varchar(1000) NOT NULL COMMENT '长链接地址',
 `long_url_md5` varchar(255) NOT NULL COMMENT '长链接地址md5',
 `deleted` tinyint(4) NOT NULL DEFAULT '0',
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 `creator` bigint(20) DEFAULT NULL,
 `updater` bigint(20) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='链接映射表';
​
-- ----------------------------
-- Table structure for visit_record
-- ----------------------------
DROP TABLE IF EXISTS `visit_record`;
CREATE TABLE `visit_record` (
 `id` bigint(20) NOT NULL COMMENT '主键',
 `url_link_id` bigint(20) NOT NULL COMMENT 'url映射id',
 `unique_code` varchar(16) NOT NULL COMMENT '紧缩码',
 `client_id` varchar(128) NOT NULL COMMENT '仅有身份标识,SHA-1(客户端IP-UA)',
 `client_ip` varchar(64) NOT NULL COMMENT '客户端IP',
 `visit_time` datetime NOT NULL COMMENT '拜访时刻',
 `user_agent` varchar(2048) DEFAULT NULL COMMENT 'UA',
 `country` varchar(32) DEFAULT NULL COMMENT '国家',
 `province` varchar(32) DEFAULT NULL COMMENT '省份',
 `city` varchar(32) DEFAULT NULL COMMENT '城市',
 `isp` varchar(32) DEFAULT NULL COMMENT '网络服务运营商',
 `browser_type` varchar(64) DEFAULT NULL COMMENT '浏览器类型',
 `browser_version` varchar(128) DEFAULT NULL COMMENT '浏览器版本号',
 `os_type` varchar(32) DEFAULT NULL COMMENT '操作系统类型',
 `device_type` varchar(32) DEFAULT NULL COMMENT '设备类型',
 `os_version` varchar(32) DEFAULT NULL COMMENT '操作系统版本号',
 `deleted` tinyint(4) DEFAULT '0' COMMENT '软删去标识',
 `creator` bigint(20) DEFAULT '0' COMMENT '创建者',
 `updater` bigint(20) DEFAULT '0' COMMENT '更新者',
 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时刻',
 `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时刻',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拜访记载';

3.2 预先生成紧缩码

@Service
@Slf4j
public class UniqueCodeServiceImpl extends ServiceImpl<UniqueCodeDAO, UniqueCode> implements UniqueCodeService {
  @Resource
  private UniqueCodeDAO uniqueCodeDAO;
  @Resource
  private StringRedisTemplate stringRedisTemplate;
  @Resource
  private IdGenerator idGenerator;
  @Resource
  private ExecutorService executorService;
  @Resource
  private ShortUrlBloomFilter shortUrlBloomFilter;
​
  @Value("${unique-code.max-size}")
  private Integer maxSize;
  @Value("${unique-code.min-size}")
  private Integer minSize;
​
​
  private static final String UNIQUE_CODE_KEY = "short_url_unique_code";
​
  @Override
  public String getUniqueCode() {
    String code = stringRedisTemplate.opsForSet().pop(UNIQUE_CODE_KEY);
    asyncGenerateUniqueCode();
    return code;
   }
​
  @Override
  public List<String> getUniqueCode(Integer size) {
    List<String> codes = stringRedisTemplate.opsForSet().pop(UNIQUE_CODE_KEY, size);
    asyncGenerateUniqueCode();
    return codes;
   }
​
  @Override
  public PageResult<UniqueCode> getUnusedList(PageParam pageParam) {
    LambdaQueryWrapper<UniqueCode> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UniqueCode::getStatus, CommonConstant.CODE_NOT_USE);
    queryWrapper.select(UniqueCode::getCode);
    PageResult<UniqueCode> pageResult = uniqueCodeDAO.selectPage(pageParam, queryWrapper);
    return pageResult;
   }
​
  @Override
  @Transactional(rollbackFor = Exception.class)
  public void generateUniqueCode() {
    log.info("==========开端生成紧缩码===========");
    long startTime = System.currentTimeMillis();
    Set<String> codes = new HashSet<>();
    Set<String> existCodes = new HashSet<>();
    for (int i = 0; i < maxSize; i++) {
      String code = RandomUtils.generateCode(6);
      Boolean exist = shortUrlBloomFilter.isExist(code);
      if (exist) {
        existCodes.add(code);
       } else {
        codes.add(code);
       }
     }
    if (!CollectionUtils.isEmpty(existCodes)) {
      log.info("=========以下紧缩码已存在:{}", existCodes);
     }
    stringRedisTemplate.opsForSet().add(UNIQUE_CODE_KEY, codes.toArray(new String[codes.size()]));
    List<UniqueCode> uniqueCodeList = new ArrayList<>();
    codes.forEach(code -> {
      UniqueCode uniqueCode = new UniqueCode();
      uniqueCode.setId(idGenerator.nextId());
      uniqueCode.setCode(code);
      uniqueCodeList.add(uniqueCode);
     });
    saveBatch(uniqueCodeList);
    long costTime = System.currentTimeMillis() - startTime;
    log.info("===============完毕生成紧缩码, costTime:[{}],Count:[{}]==============", costTime, codes.size());
   }
​
  public void asyncGenerateUniqueCode() {
    Long size = stringRedisTemplate.opsForSet().size(UNIQUE_CODE_KEY);
    // 假如剩下紧缩码数量小于最小数量,异步生成紧缩码
    if (size < minSize) {
      executorService.execute(() -> {
        generateUniqueCode();
       });
     }
​
   }
}

这儿经过装备预先生成紧缩码最大数量和最小数量,少于最小数量就会异步生成紧缩码。

3.3 生成短链接

 @Override
  public String generateShortUrl(String longUrl) {
    // 1.检验url和合法性
    if (!isValidUrl(longUrl)) {
      throw new BizException("无效的url");
     }
    // 2.判断url是否现已生成过短链接,有直接返回
    Object value = redisTemplate.opsForHash().get(LONG_MD5_CODE_MAP, longUrl);
    if (Objects.nonNull(value)) {
      return domain + value.toString();
     }
    // 3.生成短链接
    long id = idGenerator.nextId();
    String uniqueCode = uniqueCodeService.getUniqueCode();
    String longUrlMd5 = DigestUtils.md5DigestAsHex(longUrl.getBytes());
    String shortUrl = domain + uniqueCode;
    UrlLink urlLink = new UrlLink();
    urlLink.setId(id);
    urlLink.setUniqueCode(uniqueCode);
    urlLink.setShortUrl(shortUrl);
    urlLink.setLongUrl(longUrl);
    urlLink.setLongUrlMd5(longUrlMd5);
    urlLinkDAO.insert(urlLink);
    // 短链接→长链接
    redisTemplate.opsForHash().put(SHORT_LONG_MAP, uniqueCode, longUrl);
    // 长链接md5→短链接紧缩码
    redisTemplate.opsForHash().put(LONG_MD5_CODE_MAP, longUrlMd5, uniqueCode);
    return shortUrl;
   }
​

数据落库:

id  unique_code short_url long_url  long_url_md5  deleted create_time update_time creator updater
3362951037714432  j51YO8  http://127.0.0.1:18800/j51YO8 https://gitee.com/plasticene3/plasticene-boot-starter-parent  c411c28da0302d3f0c9e34872c3b66d1  0 2022-08-18 17:00:21 2022-08-18 17:00:21 1 1

3.4 短链接重定向

  @Override
  public void redirect(HttpServletRequest request, HttpServletResponse response, String uniqueCode) throws IOException {
    String longUrl = shortUrlService.getOriginUrl(uniqueCode);
    if (StringUtils.isBlank(longUrl)) {
      throw new BizException("短链接地址不存在");
     }
    executorService.execute(() -> {
      visitRecordService.addVisitRecord(request, uniqueCode);
     });
    response.sendRedirect(longUrl);
   }

异步生成链接拜访记载:

  @Override
  @Transactional(rollbackFor = Exception.class)
  public void addVisitRecord(HttpServletRequest request, String uniqueCode) {
    VisitRecord visitRecord = new VisitRecord();
    long id = idGenerator.nextId();
    visitRecord.setId(id);
    UrlLink urlLink = shortUrlService.getUrlLink(uniqueCode);
    visitRecord.setUrlLinkId(urlLink.getId());
    visitRecord.setUniqueCode(urlLink.getUniqueCode());
    visitRecord.setVisitTime(new Date());
    String agent = request.getHeader(USER_AGENT);
    String clientIp = IpUtils.getRemoteHost(request);
    IpRegion ipRegion = IpUtils.getIpRegion(clientIp);
    visitRecord.setUserAgent(agent);
    visitRecord.setClientIp(clientIp);
    // 身份仅有标识,算法:SHA-1(客户端IP + '-' + UserAgent)
    String clientId = DigestUtil.sha1Hex(clientIp + "&" + agent);
    visitRecord.setClientId(clientId);
    visitRecord.setCountry(ipRegion.getCountry());
    visitRecord.setProvince(ipRegion.getProvince());
    visitRecord.setCity(ipRegion.getCity());
    visitRecord.setIsp(ipRegion.getIsp());
​
    // 解析User-Agent
    if (StringUtils.isNotBlank(agent)) {
      try {
        UserAgent userAgent = UserAgent.parseUserAgentString(agent);
        OperatingSystem operatingSystem = userAgent.getOperatingSystem();
        // 操作系统
        Optional.ofNullable(operatingSystem).ifPresent(x -> {
          visitRecord.setOsType(x.getName());
          visitRecord.setOsVersion(x.getName());
//           visitRecord.setDeviceType(x.getDeviceType() == null ? null : x.getDeviceType().getName());
          Optional.ofNullable(x.getDeviceType()).ifPresent(deviceType -> visitRecord.setDeviceType(deviceType.getName()));
         });
        // 浏览器类型
        Browser browser = userAgent.getBrowser();
        Optional.ofNullable(browser).ifPresent(x -> visitRecord.setBrowserType(x.getGroup().getName()));
        // 浏览器版本
        Version browserVersion = userAgent.getBrowserVersion();
        Optional.ofNullable(browserVersion).ifPresent(x -> visitRecord.setBrowserVersion(x.getVersion()));
       } catch (Exception e) {
        log.error("解析UserAgent异常,事情内容:", e);
       }
     }
    visitRecordDAO.insert(visitRecord);
   }

这儿会解析客户端用户信息,用到了之前咱们讲的依据拜访IP解析归属地,依据userAgent解析设备信息等等。

数据如下:

id  url_link_id unique_code client_id client_ip visit_time  user_agent  country province  city  isp browser_type  browser_version os_type device_type os_version  deleted creator updater create_time update_time
2632861324673024  2620608022052864  ava5R7  7fb0070a4cb212b7dc0efce2cd08b017477787ae  10.8.4.7  2022-08-16 16:39:14 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 中国  浙江  杭州  电信  Chrome  104.0.0.0 Mac OS X  Computer  Mac OS X  0 1 1 2022-08-16 16:39:14 2022-08-24 18:15:18

详细代码请看:github.com/plasticene/…