短链接生成-基于布隆过滤器和唯一索引

java 复制代码
@Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 短链接接口的并发量有多少?如何测试?详情查看:https://nageoffer.com/shortlink/question
        verificationWhitelist(requestParam.getOriginUrl());
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            // 短链接项目有多少数据?如何解决海量数据存储?详情查看:https://nageoffer.com/shortlink/question
            baseMapper.insert(shortLinkDO);
            // 短链接数据库分片键是如何考虑的?详情查看:https://nageoffer.com/shortlink/question
            shortLinkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException ex) {
            // 首先判断是否存在布隆过滤器,如果不存在直接新增
            if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
                shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
            }
            throw new ServiceException(String.format("短链接:%s 生成重复", fullShortUrl));
        }
        // 项目中短链接缓存预热是怎么做的?详情查看:https://nageoffer.com/shortlink/question
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
        );
        // 删除短链接后,布隆过滤器如何删除?详情查看:https://nageoffer.com/shortlink/question
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }
   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;
    }

业务流程分步详解

1. 发起创建请求
  • 角色: 用户或客户端。
  • 动作 : 调用短链接服务的 createShortLink 接口,并传入一个原始长链接(originUrl)以及其他可选参数(如分组 ID gid、有效期 validDate 等)。
2. 参数校验
  • 角色: 短链接服务。
  • 动作 : 服务首先对接收到的 originUrl 进行合法性校验,例如检查 URL 格式是否正确,或者该 URL 是否在系统的白名单内,以防止生成非法或恶意网站的短链接。
  • 结果 :
    • 校验失败: 直接抛出异常,终止整个创建流程。
    • 校验成功: 流程继续。
3. 生成唯一短链接后缀
  • 角色 : 短链接服务(generateSuffix 方法)。
  • 动作 : 这是核心的 ID 生成环节。
    1. 循环生成: 进入一个循环,不断尝试生成一个唯一的短链接后缀。
    2. 增加随机性 : 为了避免不同请求为同一个 originUrl 生成相同的短链接,服务会在 originUrl 后拼接一个随机的 UUID
    3. 哈希与编码: 对拼接后的字符串进行哈希计算,然后将结果编码成一个简短的、URL 安全的字符串(使用 Base62 编码),得到一个候选的短链接后缀。
    4. 快速判重 : 为了避免频繁查询数据库,服务会先查询布隆过滤器 ,判断这个候选后缀是否可能已经存在。
    5. 判断与重试 :
      • 如果布隆过滤器判断不存在,则认为这个后缀是唯一的,跳出循环。
      • 如果布隆过滤器判断可能存在(可能是误判),则认为发生冲突,继续下一次循环,重新生成。
  • 结果 : 最终得到一个唯一的短链接后缀(如 abc123)。
4. 拼接完整短链接
  • 角色: 短链接服务。
  • 动作 : 将系统默认的域名(如 t.cn)和上一步生成的后缀拼接起来,形成一个完整的短链接(如 t.cn/abc123)。
5. 数据持久化与冲突处理
  • 角色: 短链接服务与数据库。
  • 动作 :
    1. 准备数据 : 创建两个数据对象(ShortLinkDOShortLinkGotoDO),分别包含短链接的完整信息和用于快速跳转的核心映射关系。
    2. 事务内插入: 在一个数据库事务中,尝试将这两个对象的数据插入到对应的两张表中。
    3. 冲突处理 :
      • 成功: 数据成功写入数据库,流程继续。
      • 失败 (DuplicateKeyException): 在极高并发下,可能两个请求同时生成了相同的短链接。第一个请求成功写入,第二个请求在写入时会因唯一键冲突而失败。服务会捕获此异常,向用户抛出 "短链接生成重复" 的业务异常,并终止流程。
6. 缓存预热
  • 角色: 短链接服务与 Redis 缓存。
  • 动作 : 为了让后续的短链接跳转请求能更快地响应,服务会立即将 完整短链接 -> 原始长链接 的映射关系存入 Redis 缓存中,并设置一个与短链接有效期相匹配的过期时间。
7. 更新布隆过滤器
  • 角色: 短链接服务与布隆过滤器。
  • 动作: 将刚刚成功创建的完整短链接添加到布隆过滤器中。这样,未来的创建请求在生成后缀时,就能通过布隆过滤器快速判断出这个短链接已经存在,从而避免了对数据库的无效查询。
8. 构建并返回响应
  • 角色: 短链接服务。
  • 动作 : 将创建成功的短链接信息(包括完整短链接、原始长链接等)封装成一个响应对象(ShortLinkCreateRespDTO),返回给用户。
1. 事务管理 (@Transactional)
  • 问题 : 你为什么在这个方法上加了 @Transactional(rollbackFor = Exception.class)
    • 考察点: 对 Spring 声明式事务的理解。
    • 深度追问 :
      • rollbackFor = Exception.class 的作用是什么?如果不加会怎么样?
        • 回答 : Spring 事务默认只对 RuntimeExceptionError 回滚。加上 rollbackFor = Exception.class 后,任何类型的 Exception(包括受检异常)都会触发回滚。这确保了在创建短链接的任何环节(如校验、数据库插入)失败时,数据库能保持一致性,不会出现只插入了部分数据的脏数据。
      • 这个事务的隔离级别是什么?在高并发场景下,这个隔离级别可能会带来什么问题?(如脏读、不可重复读、幻读)
      • 如果 stringRedisTemplate.opsForValue().set 这行代码执行失败了,数据库事务会回滚吗?为什么?
        • 回答 : 不会 。因为 @Transactional 注解只能管理数据库(如 MySQL)的事务。Redis 的操作是独立的,它不在这个数据库事务的管辖范围内。这会导致数据不一致:数据库里有记录,但缓存里没有。这是一个典型的分布式事务问题。
2. 数据库操作与并发控制
  • 问题 : 你为什么要插入两张表 (ShortLinkDOShortLinkGotoDO)?这样做有什么好处?
    • 考察点: 数据库表结构设计、读写分离思想。
    • 深度追问 :
      • ShortLinkGotoDO 这个 DO 的设计意图是什么?
        • 回答 : 这是一种典型的读写分离查询优化 设计。short_link 表存储了所有详细信息(访问统计、描述等),字段多,数据量大。而 short_link_goto 表只存储用于跳转的核心映射关系(full_short_url -> gidorigin_url)。当用户访问短链接时,查询 short_link_goto 表会更快,IO 开销更小,从而极大地提升了跳转服务的性能。
  • 问题 : 代码中 catch (DuplicateKeyException ex) 是在处理什么问题?为什么会发生这个异常?
    • 考察点: 高并发下的数据一致性问题、乐观锁思想。
    • 深度追问 :
      • 这是一种什么并发控制策略?(乐观锁 vs 悲观锁)
        • 回答 : 这是一种乐观锁 策略。它假设冲突很少发生,所以先尝试执行数据库插入操作。只有当数据库因为唯一键约束(fullShortUrlshortUri)而抛出 DuplicateKeyException 时,才认为发生了冲突。
      • 除了这种 "先操作,后处理" 的方式,还有什么其他方法可以避免或处理这种并发冲突?
        • 回答 :
          • 悲观锁: 在生成短链接前,先对某个资源(如一个计数器或特定行)加锁,确保同一时间只有一个线程能生成,这会严重影响性能。
          • 分布式锁: 使用 Redis 或 Zookeeper 等实现分布式锁,锁定一个生成区间,避免不同节点生成重复 ID。
          • 预先生成 ID: 由一个专门的 ID 生成服务(如雪花算法、号段模式)预先生成一批唯一的短链接后缀,应用服务器从 ID 服务获取 ID 再进行数据库操作,将并发冲突提前解决。

二、 缓存与性能优化

1. 布隆过滤器 (BloomFilter)
  • 问题 : 这里为什么要用布隆过滤器?它解决了什么问题?
    • 考察点: 对缓存穿透问题的理解和解决方案。
    • 深度追问 :
      • 布隆过滤器的工作原理是什么?它有什么优缺点?
        • 回答: 原理是使用多个哈希函数将元素映射到位数组的多个位置并置 1。查询时,如果所有映射位都为 1,则认为 "可能存在";如果有任何一位为 0,则 "一定不存在"。
        • 优点: 空间效率和查询时间都极高。
        • 缺点 : 存在误判率(False Positive),即它可能会把一个不存在的元素误判为存在;且一般不支持删除操作。
      • 布隆过滤器的误判率在你的代码中会导致什么后果?
        • 回答 : 如果布隆过滤器误判一个新生成的短链接已存在,generateSuffix 方法会错误地进入下一次循环,进行重试。这虽然不会导致数据错误,但会降低生成效率
      • 如果布隆过滤器服务挂了,所有数据都丢失了,你该如何恢复?
        • 回答 : 可以通过扫描数据库中所有已存在的短链接(fullShortUrl),重新构建布隆过滤器。在恢复期间,可以暂时降级,直接查询数据库进行判重,但这会对数据库造成巨大压力。
2. Redis 缓存
  • 问题 : 为什么在数据库写入成功后,还要往 Redis 里写一份缓存?
    • 考察点: 缓存的作用、读写策略。
    • 深度追问 :
      • 这属于哪种缓存更新策略?(Cache Aside Pattern)
        • 回答: 这是典型的 "先更新数据库,再更新缓存" 策略。
      • 这种策略可能会带来什么问题?
        • 回答 : 数据一致性问题。如果在更新数据库成功后、更新缓存前,有另一个请求来读取数据,它会读到缓存里的旧数据。更严重的是,如果更新缓存时失败了,就会导致数据库和缓存数据永久不一致。
      • 缓存的过期时间是如何设置的?为什么这么设计?
        • 回答 : LinkUtil.getLinkCacheValidTime(...) 表明缓存的过期时间与短链接的有效期绑定。这是一种非常合理的设计,可以自动清理掉已失效的短链接缓存,避免内存浪费。

三、 核心算法与 ID 生成 (generateSuffix 方法)

1. 哈希冲突与随机性
  • 问题 : 为什么要在 originUrl 后面拼接一个 UUID?直接对 originUrl 进行哈希不行吗?
    • 考察点: 对哈希算法特性和冲突问题的理解。
    • 深度追问 :
      • 如果不加 UUID,会出现什么问题?
        • 回答 : 如果两个不同的用户请求为同一个 originUrl 创建短链接,不加 UUID 会导致两次哈希输入完全相同,从而生成完全相同 的短链接。这可能不是我们想要的结果(我们可能希望每个请求都生成一个独立的短链接)。加了 UUID 后,每次哈希的输入都是唯一的,从根本上避免了这种因输入相同而导致的冲突。
      • 这能完全避免哈希冲突吗?
        • 回答 : 不能。哈希算法本身就存在将不同输入映射到相同输出的可能性(哈希冲突)。但 UUID 极大地降低了这种冲突的概率,使得在实际应用中可以忽略不计。
2. Base62 编码
  • 问题 : 为什么选择 Base62 而不是 Base64 或者其他编码方式?
    • 考察点: 对 URL 安全和编码效率的理解。
    • 深度追问 :
      • 回答 : Base64 包含 +, /, = 等字符,这些字符在 URL 中有特殊含义,需要进行转义(Percent-encoding),会使 URL 变长且不美观。Base62 ([0-9a-zA-Z]) 只包含 URL 安全的字符,因此生成的短链接可以直接使用,无需转义。

四、 海量数据与架构设计

  • 问题 : 如果短链接的数量达到上亿甚至十亿级别,你的数据库设计和查询方式会遇到什么挑战?如何解决?
    • 考察点: 对海量数据存储和数据库分库分表的理解。
    • 深度追问 :
      • 单表数据量过大会导致什么问题?(查询慢、索引失效、DDL 困难)
      • 你会如何进行分库分表?分库键(Sharding Key)应该如何选择?
        • 回答 : 通常会选择 gid (分组 ID) 或 fullShortUrl 作为分库分表键。
          • gid : 同一个用户或同一场景下的短链接会落在同一个分片,便于管理和统计,但可能导致数据热点(某个gid下的链接特别多)。
          • fullShortUrl 哈希分: 数据分布更均匀,能更好地打散热点,但跨分片查询会变复杂。
      • 分库分表后,SELECT * FROM short_link_goto WHERE full_short_url = ? 这样的查询如何高效执行?
        • 回答 : 如果 full_short_url 是分表键,那么可以直接根据哈希路由到对应的分片查询。如果不是,则需要进行广播查询,性能会下降。因此,short_link_goto 表非常适合用 full_short_url 作为分表键。
相关推荐
知识分享小能手几秒前
Oracle 19c入门学习教程,从入门到精通,PL/SQL 编程详解:语法、使用方法与综合案例(6)
sql·学习·oracle
難釋懷16 分钟前
Redis桌面客户端
数据库·redis·缓存
心态还需努力呀18 分钟前
国产时序数据库进入深水区:2026 年的技术分化与融合式架构趋势解析
数据库·架构·时序数据库
填满你的记忆19 分钟前
【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变
java·数据库·redis·分布式·缓存
lendsomething20 分钟前
Spring 多数据源事务管理,JPA为例
java·数据库·spring·事务·jpa
nsjqj27 分钟前
JavaEE初阶:多线程初阶(2)
java·开发语言
玩转数据库管理工具FOR DBLENS33 分钟前
人工智能:演进脉络、核心原理与未来之路 审核中
数据库·人工智能·测试工具·数据库开发·数据库架构
晓风残月淡35 分钟前
高性能MYSQL(四):查询性能优化
数据库·mysql·性能优化
cab535 分钟前
MyBatis如何处理数据库中的JSON字段
数据库·json·mybatis
黎雁·泠崖42 分钟前
Java面向对象:对象数组核心+综合实战
java·开发语言