从零实现一个短链接项目

从零实现一个短链接项目

什么是短链接?

短链接就是短网址,是在长度上比较短的网址。简单来说就是帮您把冗长的URL地址缩短成8个字符以内的短网址。

当我们在腾讯、新浪发微博时,有时发很长的网址连接,但由于微博限制字数,所以微博就自动把您发的长网址给转换成短网址了。在微博 和手机短信提醒等限制字数的地方来使用短网址,的确是一个不错的方案。

使用短链接的好处:

  1. 缩短地址长度,留足更多空间的给 有意义的内容
    URL是没有意义的,有的原始URL很长,占用有效的屏幕空间。
    微博限制字数为140字一条,那么如果这个连接非常的长,以至于将近要占用我们内容的一半篇幅,这肯定是不能被允许的,链接变短,对于有长度限制的平台发文,可编辑的文字就变多了, 所以短网址应运而生了。
  2. 可以很好的对原始URL内容管控。
    有一部分网址可以会涵盖XX,暴力,广告等信息,这样我们可以通过用户的举报,完全管理这个连接将不出现在我们的应用中,应为同样的URL通过加密算法之后,得到的地址是一样的。
  3. 可以很好的对原始URL进行行为分析
    我们可以对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于我们对项目的后续工作更好的作出决策。
  4. 短网址和短ID相当于间接提高了带宽的利用率、节约成本
  5. 链接太长在有些平台上无法自动识别为超链接
  6. 短链接更加简洁好看且安全,不暴露访问参数。而且,能规避关键词、域名屏蔽等手段

例如:

这是原始链接:

https://blog.csdn.net/qq_68883928/article/details/144195187?spm=1001.2014.3001.5501

这是短链接:

http://124.222.97.64:19501/api/v1/url/SWASF

好处无用置疑

短链接系统的原理

短URL系统的核心: 将长的 URL 转化成短的 URL

客户端在访问系统时,短URL的工作流程如下:

  • 先使用短地址A访问 短链Java 服务
  • 短链Java 服务 进行 地址转换和映射,将 短URL系统映射到对应的长地址URL
  • 短链Java 服务 返回302 重定向 给客户端
  • 然后客户端再重定向到原始服务

长URL如何变短?

原始URL如何变短呢?简单来说, 可以将原始的地址,使用编号进行替代

编号如何进一步变短呢? 可以使用更大的进制来表示

六十二进制表示法

顾名思义短网址就是非常短的网址,比如http://xxx.cn/ETOFYE,其中核心的部分 ETOFYE 只有6位长度。

62进制 最大竟能生成 62 ^ 6 - 1 =56800235583个 基本上够了。

即使6位长度62^6也能达到568亿的范围,

这样的话只要算法得当,可以覆盖很大的数据范围。

在编码的过程中,可以按照自己的需求来调整62进制各位代表的含义。

一个典型的场景是, 在编码的过程中,如果不想让人明确知道转换前是什么,可以进行弱加密,

比如A站点将字母c表示32、B站点将字母c表示60,就相当于密码本了。

短URL系统的功能分析

假设短地址长度为6位,62的6次方足够一般系统使用了

系统核心实现,包含三个大的功能:
  • 发号
  • 存储
  • 映射

可以分为两个模块:发号与存储模块、映射模块

发号与存储模块
  • 发号:使用发号器发号 , 为每个长地址分配一个号码ID,并且需要防止地址二义,也就是防止同一个长址多次请求得到的短址不一样
  • 存储:将号码与长地址存放在DB中,将号码转化成62进制,用于表示最终的短地址,并返回给用户
映射模块

用户使用62进制的短地址请求服务 ,

  • 转换:将62进制的数转化成10进制,因为咱们系统内部是long 类型的10进制的数字ID
  • 映射:在DB中寻找对应的长地址
  • 通过302重定向,将用户请求重定向到对应的地址上

发号器的高并发架构

回顾一下发号器的功能:

  • 为每个长地址分配一个号码ID
  • 并且需要防止地址歧义

以下仅对本文使用的分布式ID方案做简单介绍

snowflake算法(雪花算法)生成ID

snowflake ID 严格来说,属于 本地生产 ID,这点和 Redis ID、MongoDB ID不同, 后者属于远程生产的ID。

本地生产ID性能高,远程生产的ID性能低。

snowflake ID原理是使用Long类型(64位),按照一定的规则进行分段填充:时间(毫秒级)+集群ID+机器ID+序列号,每段占用的位数可以根据实际需要分配,其中集群ID和机器ID这两部分,在实际应用场景中要依赖外部参数配置或数据库记录。

总结一下,snowflake ID 的优缺点和使用场景:

  • 优点:
    高性能、低延迟、去中心化、按时间总体有序
  • 缺点:
    要求机器时钟同步(到秒级即可),需要解决 时钟回拨问题
    如果某台机器的系统时钟回拨,有可能造成 ID 冲突,或者 ID 乱序。
  • 适用场景:
    分布式应用环境的数据主键

二义性检查的高并发架构

所谓的地址二义性,就行同一个长址多次请求得到的短址不一样。

在生产地址的时候,需要进行二义性检查,防止每次都会重新为该长址生成一个短址,一个个长址多次请求得到的短址是不一样。

通过二义性检查,实现长短链接真正意义上的一对一。

怎么进行 二义性检查?

最简单,最为粗暴的方案是:直接去数据库中检查

但是,这就需要付出很大的性能代价。

要知道:

数据库主键不是 原始url,而是 短链url 。

如果根据 原始url 去进行存在性检查,还需要额外建立索引。

问题的关键是,数据库性能特低,没有办法支撑超高并发 二义性检查

所以,这里肯定不能每次用数据库去检查。

这里很多同学可能会想到另一种方案,就是 redis 的布隆过滤, 把已经生成过了的 原始url,

大致的方案是,可以把已经生成过的 原始url ,在 redis 布隆过滤器中进行记录。

布隆过滤器

布隆过滤器就是bitset+多次hash的架构,宏观上是空间换时间,不对所有的 surl (原始url)进行内容存储,只对surl进行存在性存储,这样就节省大家大量的内存空间。

在数据量比较大的情况下,既满足时间要求,又满足空间的要求。

但是布隆过滤器有个缺点:就是存在不一定存在,不存在一定不存在。

Bloom Filter 相当于是一个不太精确的 set 集合,我们可以利用它里边的 contains 方法去判断某一个对象是否存在,但是需要注意,这个判断不是特别精确。

一般来说,通过 contains 判断某个值不存在,那就一定不存在,但是判断某个值存在的话,则他可能不存在。

使用缓存架构

具体来说,可以使用 Redis 缓存进行 热门url的缓存,实现部分地址的一对一缓存。

在进行生成短链接时,对长URL和短URL进行映射,防止重复的生成请求,在使用布隆过滤器对长URL进行二次判断,从而减少mysql崩溃概率。

进行短链接跳转时,将短链接与长链接进行映射,提高系统的响应速度。

具体实现

pojos实体类

java 复制代码
/**
 * url实体类
 */
@Data
@TableName("url_mapping")
public class UrlMapping implements Serializable {
    private Long id; // 主键
    private String shortUrl; // 短链key
    private String userId ;//所属用户id
    private String longUrl; // 原始url
    private Date expireTime; // key失效日期
    private Date creatTime; // 创建日期
    private Date updateTime; // 修改日期
}

controller层

java 复制代码
/**
 * @author 
 * @deprecated url转化对应接口
 * @date
 */

@RestController
@RequestMapping("/api/v1/url")
public class UrlController {

    @Autowired
    UrlService urlIService;

    /**
     * 将长链接变成短链接的接口
     * @param dto
     * @return
     */
    @PostMapping("/getUrl")
    public Result getUrl(@NotNull @RequestBody UrlRequestDto dto) {
        return  urlIService.getUrl(dto);
    }

    /**
     * 通过短连接访问源链接
     * @param url
     * @return
     */
    @GetMapping("/{url}")
    public Result toUrl(@PathVariable("url") String url, HttpServletResponse response){
        return  urlIService.toUrl(url,response);
    }
}

service层

java 复制代码
public interface UrlService extends IService<UrlMapping> {
    Result getUrl(UrlRequestDto dto);

    Result toUrl(String url, HttpServletResponse response);
}
实现类
java 复制代码
@Service
@Transactional
@Slf4j
public class UrlServiceImpl extends ServiceImpl<UrlMapper, UrlMapping> implements UrlService {

    @Autowired
    RBloomFilter rbloomFilter;

    @Autowired
    UrlMapper urlMapper;

    @Autowired
    UserService userService;

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 根据长链接获取短链接
     * @param dto
     * @return
     */
    @Cacheable(cacheNames = RedisConstants.LONGURL_TO_SHORTURL ,key = "#dto.url",sync = true)
    public Result getUrl(UrlRequestDto dto){
        //TODO:此处为本项目核心
        //TODO:1、获取appkey额度是否存在和超额
        //2、使用布隆过滤器判断生成的短URl是否存在
        try {
            if (!rbloomFilter.contains(dto.getUrl())) {
                //不存在
                log.info("请求参数为:"+dto);
                String longUrl = urlMapper.selectByLongUrl(dto.getUrl());
                if(StringUtils.isNotBlank(longUrl)){
                    return Result.success(longUrl);
                }
                String response = NoExistUrl(dto);
                if(response.equals("-1")){
                    return Result.error(ResponseStatusMessage.User_IS_NOT_LOGIN);
                }
                return Result.success(response);
            } else {
                //存在
                //数据库兜底
                String shortUrl = urlMapper.selectByLongUrl(dto.getUrl());
                if (StringUtils.isEmpty(shortUrl)) {
                    String response = NoExistUrl(dto);
                    if(response.equals("-1")){
                        return Result.error(ResponseStatusMessage.User_IS_NOT_LOGIN);
                    }
                    return Result.success(response);
                }
                return Result.success(shortUrl);
            }
        } catch (Exception e) {
            log.error("长链接生成短连接失败");
            throw new RuntimeException(e);
        }
    }

    /**
     * 通过短连接跳转链接
     * @param url
     * @param response
     * @return
     */
    public Result toUrl(String url, HttpServletResponse response) {
        if (StringUtils.isBlank(url)) {
            return Result.error(ResponseStatusMessage.Url_IS_NOT_EMPTY);
        }
        String longurl = (String)redisTemplate.opsForValue().get(RedisConstants.SHORTURL_TO_LONGRUL + url);
        try {
            if(StringUtils.isNotBlank(longurl)){
                userService.ShortUrlCount(url);
                response.sendRedirect(longurl);
                return Result.success();
            }
            longurl = urlMapper.selectByShortUrl(url);
            redisTemplate.opsForValue().set(RedisConstants.SHORTURL_TO_LONGRUL + url,longurl);
            if (StringUtils.isBlank(longurl)) {
                log.info("通过短连接跳转链接中,查询数据库异常:无该短连接对应的长链接:"+longurl);
                return Result.error(ResponseStatusMessage.SHORTURL_TO_LONGURL_ENPTY);
            }
            userService.ShortUrlCount(url);
            response.sendRedirect(longurl);
            return Result.success();
        } catch (Exception e) {
            return Result.error(ResponseStatusMessage.UNKNOW_ANOMALY);
        }
    }

    @NotNull
    private String NoExistUrl(UrlRequestDto dto) {
        UserAppKey user = DbContextHolder.getUser();
        if(ObjectUtils.isEmpty(user)){
            return new  StringBuffer().append("-1").toString();
        }
        String shortUrl = UrlToShortHash(dto.getUrl());
        rbloomFilter.add(shortUrl);
        UrlMapping urlMapping = new UrlMapping();
        urlMapping.setLongUrl(dto.getUrl());

        urlMapping.setUserId(user.getUserId());
        urlMapping.setShortUrl(shortUrl);
        urlMapping.setCreatTime(Calendar.getInstance().getTime());
        urlMapper.insert(urlMapping);
        return shortUrl;
    }

}

最少6位的短码

java 复制代码
/**
 * url变短的hash生成短url
 * @return
 */
private static final String ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = 62;
public  static final String UrlToShortHash(String url){
    // 生成 32 位的 Murmur3 哈希值
    long hash = Hashing.murmur3_32().hashUnencodedChars(url).padToLong();
    // 将 32 位哈希值转换为 62 进制字符串
    StringBuilder sb = new StringBuilder();
    while (hash > 0) {
        sb.append(ALPHABET.charAt((int) (hash % BASE)));
        hash /= BASE;
    }
    // 确保生成的短 URL 至少有 6 个字符
    while (sb.length() < 6) {
        sb.append(ALPHABET.charAt(0));
    }
    // 反转字符串,使其更美观
    return sb.reverse().toString();
}

布隆过滤器

java 复制代码
/**
 * 短链接布隆过滤器,用于减少数据库的压力
 */
@Component
@Slf4j
public class UrlBloomFilter  {

    @Autowired
    private RedissonClient redissonClient;
    /**
     * 编码
     */
    @Value("${Bloom.charset}")
    private String CharSet;
    /**
     * 最大处理的数量
     */
    @Value("${Bloom.maxnums}")
    private int MaxNums;
    /**
     * 需要控制的错误概率
     */
    @Value("${Bloom.errpre}")
    private Double ErrPre;

    /**
     * 创建布隆过滤器
     * @return
     */
    @Bean("RBloomFilter")
    @Primary
    public  RBloomFilter  InitR( ) {
        RBloomFilter bloomFilter = redissonClient.getBloomFilter("UrlBloomFilter");
        bloomFilter.tryInit(MaxNums, ErrPre);
        return bloomFilter;
    }
}

缓存实现

java 复制代码
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient InitRedisson(){
        Config config = new Config();
        if (StringUtils.isNotBlank(password)) {
            config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        } else {
            config.useSingleServer().setAddress("redis://" + host + ":" + port);
        }
        //添加主从配置
//        config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
        return Redisson.create(config);
    }
}

配置文件

yaml 复制代码
Bloom:
  charset: utf-8
  maxnums: 100000
  errpre: 0.000001
sping:
  datasource:
    druid:
      type: com.alibaba.druid.pool.DruidDataSource
      cs1-db:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/short-chain?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 
        initialSize: 10
        minIdle: 20
        maxActive: 100
        maxWait: 60000
  redis:
    host: 127.0.0.1
    port: 6379
    password:
  cache:
    type: redis   # 指定使用的缓存类型

启动类上添加注解

java 复制代码
@SpringBootApplication
@EnableCaching
public class application {
    public static void main(String[] args) {
        SpringApplication.run(application.class, args);
        System.out.println("     _                                  \n" +
                "                              _ooOoo_                               \n" +
                "                             o8888888o                              \n" +
                "                             88\" . \"88                              \n" +
                "                             (| -_- |)                              \n" +
                "                             O\\  =  /O                              \n" +
                "                          ____/`---'\\____                           \n" +
                "                        .'  \\\\|     |//  `.                         \n" +
                "                       /  \\\\|||  :  |||//  \\                        \n" +
                "                      /  _||||| -:- |||||_  \\                       \n" +
                "                      |   | \\\\\\  -  /'| |   |                       \n" +
                "                      | \\_|  `\\`---'//  |_/ |                       \n" +
                "                      \\  .-\\__ `-. -'__/-.  /                       \n" +
                "                    ___`. .'  /--.--\\  `. .'___                     \n" +
                "                 .\"\" '<  `.___\\_<|>_/___.' _> \\\"\".                  \n" +
                "                | | :  `- \\`. ;`. _/; .'/ /  .' ; |    Buddha       \n" +
                "                \\  \\ `-.   \\_\\_`. _.'_/_/  -' _.' /                 \n" +
                "  ================-.`___`-.__\\ \\___  /__.-'_.'_.-'================  \n" +
                "                              `=--=-'                佛祖保佑无bug---王五周八        ");
    }
}

依赖

xml 复制代码
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
<!--        布隆过滤器的依赖-->
        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0.1-jre</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.22</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.32</version>
            <scope>runtime</scope>
        </dependency>
        <!-- 其他依赖项 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.0</version>
        </dependency>
            <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.6.4</version>
        </dependency>

结尾

由上述可以实现一个基于布隆过滤器和redis实现的一个简易的短链接系统,这只实现了短链接的核心功能,有兴趣的小伙伴还可以基于以上进行扩展,从而达到商用的目的。

相关推荐
2401_8337880541 分钟前
Scala的模式匹配(2)
java·开发语言
Lalolander2 小时前
2024信创数据库TOP30之华为Gauss DB
大数据·数据库·科技·华为·系统架构
悠悠龙龙2 小时前
框架模块说明 #05 权限管理_03
java·开发语言·spring
开心羊咩咩3 小时前
Idea 2024.3 突然出现点击run 运行没有反应,且没有任何提示。
java·ide·intellij-idea
waterme1onY3 小时前
IDEA中MAVEN的一些设置问题
java·maven·intellij-idea
莳花微语3 小时前
Oracle 11G DataGuard GAP 修复过程(通过主库scn增备恢复)
数据库·oracle
阿华的代码王国3 小时前
【算法】——前缀和(矩阵区域和详解,文末附)
java·开发语言·算法·前缀和
petaexpress3 小时前
云原生和数据库哪个好一些?
数据库·云原生·云原生和数据库哪个好·云原生和数据库
梦.清..3 小时前
面向对象(二)——类和对象(上)
java
Cristiano永远是goat3 小时前
数据库原理-期末复习基础知识第二弹
数据库