当前位置: 首页 > news >正文

Java短链接生成服务实战指南

1. 短链接服务的实际应用

1.1 为什么需要短链接

短链接在现代互联网应用中扮演着重要角色。
想象一下,你在发短信推广时,一个几十个字符的长链接会占用大量字符空间。
而短链接只需要几个字符就能搞定,既节省成本又提升用户体验。

在社交媒体分享时,短链接让内容看起来更简洁。
微博的140字限制下,每个字符都很珍贵。
短链接还能帮你追踪点击数据,了解推广效果。

1.2 短链接的核心原理

短链接的工作机制其实很简单:建立一个映射关系。
长链接通过某种算法生成一个短标识,存储在数据库中。
用户访问短链接时,系统根据短标识找到原始链接,然后重定向过去。

这个过程就像给每个长链接发了一张身份证,凭证就能找到本人。

2. 两种生成策略对比

2.1 随机生成策略

随机生成就是用算法随机产生短链接标识。
这种方式实现简单,但有个问题:可能会撞车。
所以需要额外检查生成的标识是否已经存在。

2.2 自增生成策略

自增方式使用一个递增的数字作为基础。
每次生成新的短链接,数字就加1,然后转换成短标识。
这样能保证不重复,但在分布式环境下需要考虑并发问题。

3. 随机生成方式实现

3.1 核心代码实现

随机生成方式的核心思路是使用62进制字符集来构造短标识。
我们选择62进制是因为它包含了数字、小写字母和大写字母,既保证了足够的组合数量,又避免了特殊字符带来的URL编码问题。
下面的代码展示了完整的随机生成实现:

@RestController
@RequestMapping("/shortUrl")
public class ShortUrlController {// 定义62进制字符集:数字+小写字母+大写字母private String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";// 短链接域名前缀private String shortUrlPrefix = "http://a.cn/";// 存储短链接和长链接的映射关系private HashMap<String, String> map = new HashMap<>();@RequestMapping("getShortUrl")public String getShortUrl(String longUrl) {// 参数校验:确保传入的URL不为空if (longUrl == null || longUrl.trim().isEmpty()) {throw new IllegalArgumentException("URL不能为空");}String key = createKey();// 如果生成的key已存在,重新生成直到唯一// 注意:在生产环境中应该限制重试次数,避免无限循环int retryCount = 0;while (map.containsKey(key) && retryCount < 10) {key = createKey();retryCount++;}if (retryCount >= 10) {throw new RuntimeException("生成短链接失败,请重试");}map.put(key, longUrl);return shortUrlPrefix + key;}@RequestMapping("getLongUrl")public String getLongUrl(String shortUrl) {// 参数校验:确保短链接格式正确if (shortUrl == null || !shortUrl.startsWith(shortUrlPrefix)) {throw new IllegalArgumentException("短链接格式不正确");}// 提取短链接标识,去掉域名前缀String shortKey = shortUrl.replace(shortUrlPrefix, "");String longUrl = map.get(shortKey);// 如果找不到对应的长链接,返回null或抛出异常if (longUrl == null) {throw new RuntimeException("短链接不存在或已过期");}return longUrl;}private String createKey() {Random rand = new Random();StringBuilder sb = new StringBuilder();// 生成6位随机字符串,每位从62个字符中随机选择for (int i = 0; i < 6; i++) {// 随机选择一个索引位置,范围是0-61int randomIndex = rand.nextInt(62);// 根据索引从字符集中取出对应字符sb.append(BASE62.charAt(randomIndex));}return sb.toString();}
}

3.2 代码解析

这个实现方案有几个关键设计点需要理解:

字符集选择原理:
BASE62字符集包含62个字符,这样6位字符串就能产生62^6种组合。
理论上能生成568亿个不同的短链接,对大多数应用来说够用了。
选择62进制而不是64进制,是为了避免URL中的特殊字符如’+‘和’/'。

重复检测机制:
createKey()方法每次随机选择6个字符组成标识。
虽然有重复的可能,但概率很小,通过while循环能确保唯一性。
在实际生产中,这个检测应该放在数据库层面,而不是内存HashMap。

4. 自增生成方式实现

4.1 主控制器代码

自增方式的优势在于能够保证生成的短链接绝对不重复。
我们从一个较大的数字开始(比如100万),这样生成的短链接不会太短,看起来更专业。
每次生成新链接时,数字递增1,然后转换为Base62编码:

@RestController
@RequestMapping("/shortUrl2")
public class ShortUrl2Controller {private String shortUrlPrefix = "http://a.cn/";private HashMap<String, String> map = new HashMap<>();// 从100万开始,避免短链接太短private Long num = 1000000L;@RequestMapping("getShortUrl")public String getShortUrl(String longUrl) {String key = createKey();map.put(key, longUrl);return shortUrlPrefix + key;}@RequestMapping("getLongUrl")public String getLongUrl(String shortUrl) {return map.get(shortUrl.replace(shortUrlPrefix, ""));}private String createKey() {String base62 = Base62Util.base62Encode(num);num++;return base62;}
}

4.2 Base62编码工具类

Base62编码是整个自增方案的核心算法。
它的作用是将十进制数字转换成62进制字符串,从而大大缩短标识的长度。
这个转换过程类似于我们熟悉的十进制转二进制,只是进制基数不同:

public class Base62Util {private static final String BASE62_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";public static String base62Encode(long num) {List<Character> base62Digits = new ArrayList<>();// 进制转换的核心算法:不断除以62取余数do {// 取余数,得到当前位的数字(0-61)int remainder = (int) (num % 62);// 将数字转换为对应的字符并存储base62Digits.add(BASE62_CHARACTERS.charAt(remainder));// 继续处理商,相当于右移一位num /= 62;} while (num > 0);StringBuilder sb = new StringBuilder();// 反转数字顺序,得到正确的Base62编码// 因为我们是从低位到高位计算的,需要反转才是正确顺序for (int i = base62Digits.size() - 1; i >= 0; i--) {sb.append(base62Digits.get(i));}return sb.toString();}
}

4.3 编码原理说明

Base62编码就是把十进制数字转换成62进制表示。
这个转换过程可以用一个具体例子来理解:

转换示例:
数字1000000的转换过程:

  • 1000000 ÷ 62 = 16129,余数 8 → 字符’8’
  • 16129 ÷ 62 = 260,余数 9 → 字符’9’
  • 260 ÷ 62 = 4,余数 12 → 字符’c’
  • 4 ÷ 62 = 0,余数 4 → 字符’4’
  • 反转后得到:“4c98”

这样就把7位数字压缩成了4位字符串,压缩率非常高。
每次num自增,确保生成的短链接都不重复。

5. 接口使用示例

5.1 生成短链接

访问接口:GET /shortUrl/getShortUrl?longUrl=https://www.example.com

返回结果:http://a.cn/aBc123

5.2 获取原始链接

访问接口:GET /shortUrl/getLongUrl?shortUrl=http://a.cn/aBc123

返回结果:https://www.example.com

6. 生产环境优化策略

6.1 分布式ID生成

在真实的生产环境中,单机自增肯定不够用。
多台服务器同时生成ID会导致冲突,这时候就需要分布式ID生成方案。
雪花算法是Twitter开源的分布式ID生成算法,它能保证在分布式环境下生成全局唯一的ID。

雪花算法的组成结构:

  • 1位符号位(固定为0)
  • 41位时间戳(毫秒级,可用69年)
  • 10位机器标识(5位数据中心ID + 5位工作机器ID)
  • 12位序列号(同一毫秒内的递增序列)
@Component
public class SnowflakeIdGenerator {private final long workerId;private final long datacenterId;private long sequence = 0L;private long lastTimestamp = -1L;public SnowflakeIdGenerator(long workerId, long datacenterId) {this.workerId = workerId;this.datacenterId = datacenterId;}public synchronized long nextId() {long timestamp = System.currentTimeMillis();// 检查时钟回拨,这在分布式环境中可能发生if (timestamp < lastTimestamp) {throw new RuntimeException("时钟回拨异常");}// 如果是同一毫秒内的请求,序列号递增if (lastTimestamp == timestamp) {// 序列号递增,使用位运算确保不超过12位(4095)sequence = (sequence + 1) & 4095; // 4095 = 2^12 - 1// 如果序列号溢出,等待下一毫秒if (sequence == 0) {timestamp = waitNextMillis(lastTimestamp);}} else {// 新的毫秒,序列号重置为0sequence = 0L;}lastTimestamp = timestamp;// 组装64位ID:时间戳(41位) + 数据中心ID(5位) + 工作机器ID(5位) + 序列号(12位)return ((timestamp - 1609459200000L) << 22) // 时间戳左移22位(减去起始时间戳)| (datacenterId << 17)                // 数据中心ID左移17位| (workerId << 12)                    // 工作机器ID左移12位| sequence;                           // 序列号占最低12位}private long waitNextMillis(long lastTimestamp) {// 等待下一毫秒,确保时间戳递增// 这个方法在高并发场景下可能会消耗CPU,但保证了ID的唯一性long timestamp = System.currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = System.currentTimeMillis();}return timestamp;}
}

6.2 Redis缓存优化

数据库查询是短链接服务的性能瓶颈,特别是在高并发场景下。
Redis作为内存数据库,查询速度比MySQL快几个数量级。
我们可以将热点数据缓存到Redis中,大幅提升响应速度。

缓存策略设计:

  • 设置合理的过期时间,避免缓存无限增长
  • 使用统一的key前缀,便于管理和清理
  • 提供exists方法,快速判断短链接是否存在
@Service
public class ShortUrlService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private static final String SHORT_URL_PREFIX = "short_url:";private static final int EXPIRE_TIME = 7 * 24 * 3600; // 7天过期public void saveMapping(String shortKey, String longUrl) {// 构造Redis键名,使用前缀避免键冲突String redisKey = SHORT_URL_PREFIX + shortKey;// 设置键值对,同时设置过期时间防止内存泄漏redisTemplate.opsForValue().set(redisKey, longUrl, EXPIRE_TIME, TimeUnit.SECONDS);}public String getLongUrl(String shortKey) {// 根据短链接标识查询原始URLString redisKey = SHORT_URL_PREFIX + shortKey;return redisTemplate.opsForValue().get(redisKey);}public boolean exists(String shortKey) {// 快速判断短链接是否存在,避免无效查询String redisKey = SHORT_URL_PREFIX + shortKey;return Boolean.TRUE.equals(redisTemplate.hasKey(redisKey));}
}

6.3 布隆过滤器防重复

当数据量达到千万级别时,即使是Redis查询也会成为瓶颈。
布隆过滤器是一种空间效率极高的概率型数据结构,用于快速判断元素是否存在。
它的特点是:如果说不存在,那一定不存在;如果说存在,可能存在误判。

布隆过滤器的优势:

  • 空间占用极小,100万元素只需要几MB内存
  • 查询速度极快,时间复杂度O(1)
  • 适合作为第一层过滤,减少数据库查询
@Component
public class BloomFilterService {private BloomFilter<String> bloomFilter;@PostConstructpublic void init() {// 初始化布隆过滤器,需要预估数据量和可接受的误判率// 预估100万个元素,误判率0.01%(万分之一)// 误判率越低,占用内存越大,需要根据实际情况平衡bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), // 字符串哈希函数1000000,    // 预期元素数量0.0001      // 误判率(0.01%));}public boolean mightContain(String url) {return bloomFilter.mightContain(url);}public void put(String url) {bloomFilter.put(url);}
}

6.4 三层架构应用

在实际生产环境中,我们采用"布隆过滤器 + Redis + 数据库"的三层架构来优化查询性能:

@Service
public class ShortUrlService {@Autowiredprivate BloomFilterService bloomFilterService;@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Autowiredprivate ShortUrlMapper shortUrlMapper;public String getLongUrl(String shortKey) {// 第一层:布隆过滤器快速过滤不存在的数据// 如果布隆过滤器说不存在,那一定不存在,直接返回if (!bloomFilterService.mightContain(shortKey)) {return null; // 避免无效的Redis和数据库查询}// 第二层:Redis缓存查询String redisKey = "short_url:" + shortKey;String longUrl = redisTemplate.opsForValue().get(redisKey);if (longUrl != null) {return longUrl; // 缓存命中,直接返回}// 第三层:数据库查询(布隆过滤器可能误判)ShortUrl shortUrl = shortUrlMapper.selectByShortKey(shortKey);if (shortUrl != null) {// 查询到数据,回写Redis缓存redisTemplate.opsForValue().set(redisKey, shortUrl.getLongUrl(), Duration.ofHours(24)); // 缓存24小时return shortUrl.getLongUrl();}return null; // 确实不存在}public String createShortUrl(String longUrl) {String shortKey = generateShortKey();// 保存到数据库ShortUrl shortUrl = new ShortUrl();shortUrl.setShortKey(shortKey);shortUrl.setLongUrl(longUrl);shortUrlMapper.insert(shortUrl);// 同步更新布隆过滤器和Redis缓存bloomFilterService.put(shortKey);String redisKey = "short_url:" + shortKey;redisTemplate.opsForValue().set(redisKey, longUrl, Duration.ofHours(24));return shortKey;}
}

这种三层架构的优势:

  • 布隆过滤器:过滤99%以上的无效请求,保护后端系统
  • Redis缓存:处理热点数据访问,响应时间在毫秒级
  • 数据库:作为最终数据源,保证数据一致性

7. 数据库设计

7.1 短链接表结构

-- 短链接数据表设计,考虑了性能和扩展性
CREATE TABLE short_url (id BIGINT PRIMARY KEY AUTO_INCREMENT,                    -- 主键,自增IDshort_key VARCHAR(10) NOT NULL UNIQUE COMMENT '短链接标识',  -- 短链接标识,唯一索引long_url TEXT NOT NULL COMMENT '原始长链接',                -- 原始URL,使用TEXT支持长URLcreate_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  -- 创建时间,自动填充expire_time TIMESTAMP NULL COMMENT '过期时间',             -- 过期时间,可为空表示永不过期click_count INT DEFAULT 0 COMMENT '点击次数',              -- 点击统计,默认为0INDEX idx_short_key (short_key),                        -- 短链接查询索引(最重要)INDEX idx_create_time (create_time)                     -- 时间范围查询索引
);

7.2 数据访问层实现

数据访问层负责与数据库的交互操作。
这里使用JdbcTemplate而不是JPA,是为了更好地控制SQL语句,提升查询性能。
特别注意expire_time的处理,确保过期的短链接不会被查询到:

@Repository
public class ShortUrlRepository {@Autowiredprivate JdbcTemplate jdbcTemplate;public void save(String shortKey, String longUrl) {String sql = "INSERT INTO short_url (short_key, long_url) VALUES (?, ?)";jdbcTemplate.update(sql, shortKey, longUrl);}public String findLongUrl(String shortKey) {// 查询SQL包含过期时间检查,确保不返回过期的链接// expire_time IS NULL 表示永不过期// expire_time > NOW() 表示还未过期String sql = "SELECT long_url FROM short_url WHERE short_key = ? AND (expire_time IS NULL OR expire_time > NOW())";try {return jdbcTemplate.queryForObject(sql, String.class, shortKey);} catch (EmptyResultDataAccessException e) {// 没有找到记录时返回null,而不是抛出异常return null;}}public void incrementClickCount(String shortKey) {String sql = "UPDATE short_url SET click_count = click_count + 1 WHERE short_key = ?";jdbcTemplate.update(sql, shortKey);}
}

8. 性能监控与统计

8.1 点击统计功能

短链接服务不仅要提供跳转功能,还要统计访问数据。
这对于营销推广和数据分析非常重要。
我们使用异步更新来避免统计操作影响跳转性能,用户体验优先:

@RestController
public class ShortUrlRedirectController {@Autowiredprivate ShortUrlService shortUrlService;@Autowiredprivate ShortUrlRepository repository;@GetMapping("/s/{shortKey}")public ResponseEntity<Void> redirect(@PathVariable String shortKey) {String longUrl = shortUrlService.getLongUrl(shortKey);if (longUrl == null) {return ResponseEntity.notFound().build();}// 异步更新点击统计,不阻塞用户跳转// 使用CompletableFuture确保统计操作在后台执行CompletableFuture.runAsync(() -> {try {repository.incrementClickCount(shortKey);} catch (Exception e) {// 统计失败不影响用户体验,只记录日志logger.error("更新点击统计失败: shortKey={}", shortKey, e);}});HttpHeaders headers = new HttpHeaders();headers.setLocation(URI.create(longUrl));return new ResponseEntity<>(headers, HttpStatus.FOUND);}
}

8.2 访问日志记录

详细的访问日志对于分析用户行为和排查问题非常重要。
通过拦截器的方式,我们可以统一记录所有短链接的访问情况。
特别要注意获取真实IP地址,考虑代理和负载均衡的情况:

@Component
public class AccessLogInterceptor implements HandlerInterceptor {private static final Logger logger = LoggerFactory.getLogger(AccessLogInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String shortKey = extractShortKey(request.getRequestURI());String userAgent = request.getHeader("User-Agent");String clientIp = getClientIp(request);// 记录访问日志logger.info("短链接访问: shortKey={}, ip={}, userAgent={}", shortKey, clientIp, userAgent);return true;}private String getClientIp(HttpServletRequest request) {// 获取真实客户端IP,考虑代理和负载均衡的情况// X-Forwarded-For是代理服务器添加的头部,包含真实客户端IPString xForwardedFor = request.getHeader("X-Forwarded-For");if (xForwardedFor != null && !xForwardedFor.isEmpty()) {// 可能包含多个IP,第一个是真实客户端IPreturn xForwardedFor.split(",")[0].trim();}// 如果没有代理,直接获取远程地址return request.getRemoteAddr();}private String extractShortKey(String uri) {return uri.substring(uri.lastIndexOf('/') + 1);}
}

9. 总结

小型应用(日访问量 < 10万):

  • 使用随机生成策略,简单易实现
  • 数据存储用MySQL即可
  • 不需要复杂的缓存和分布式方案

中型应用(日访问量 10万-100万):

  • 推荐自增生成策略,避免重复检测开销
  • 引入Redis缓存热点数据
  • 考虑数据库读写分离

大型应用(日访问量 > 100万):

  • 必须使用分布式ID生成(雪花算法)
  • 多级缓存架构(本地缓存 + Redis)
  • 布隆过滤器预过滤
  • 数据库分库分表

这套短链接服务涵盖了从基础实现到生产优化的完整方案。
在实际使用中,可以根据业务需求选择合适的生成策略和优化方案。
简单的方案往往是最好的方案,不要过度设计。

参考:https://blog.csdn.net/java_zhangshuai/article/details/106942758


文章转载自:

http://vcjEZAGa.gjwkL.cn
http://vaVZi4yQ.gjwkL.cn
http://EAiI7I50.gjwkL.cn
http://08KCLz41.gjwkL.cn
http://zNXyXasn.gjwkL.cn
http://rzkY87JO.gjwkL.cn
http://wPelOCVZ.gjwkL.cn
http://17KmFoz1.gjwkL.cn
http://GS5k9zBo.gjwkL.cn
http://qUw63wII.gjwkL.cn
http://yFjAPaJB.gjwkL.cn
http://Mz9mRvhU.gjwkL.cn
http://q6mU36nP.gjwkL.cn
http://M62dp14D.gjwkL.cn
http://JUNjvGqQ.gjwkL.cn
http://LIKVD2Iz.gjwkL.cn
http://B9OTADQF.gjwkL.cn
http://j4RHnX6Q.gjwkL.cn
http://uEgv5X5H.gjwkL.cn
http://RYEK31tn.gjwkL.cn
http://aPLV1gcM.gjwkL.cn
http://83359AH2.gjwkL.cn
http://T3BWADwv.gjwkL.cn
http://THnmLzNa.gjwkL.cn
http://hdPXcR84.gjwkL.cn
http://gKnIq9ld.gjwkL.cn
http://5gMH58FB.gjwkL.cn
http://B3cm2y8h.gjwkL.cn
http://JkLSKJMh.gjwkL.cn
http://AcqIVWhG.gjwkL.cn
http://www.dtcms.com/a/379813.html

相关文章:

  • JAVA Web —— A / 网页开发基础
  • TensorFlow深度学习实战:从零开始构建你的第一个神经网络
  • Keepalived 负载均衡
  • 智能文档处理业务,应该选择大模型还是OCR专用小模型?
  • 《Redis核心机制解析》
  • Netty 在 API 网关中的应用篇(请求转发、限流、路由、负载均衡)
  • 金蝶云星空插件开发记录(一)
  • Knockout-ES5 入门教程
  • 基于 Art_DAQ、InfluxDB 和 PyQt 的传感器数据采集、存储与可视化
  • 【图像处理基石】图像压缩有哪些经典算法?
  • C语言实战:简单易懂通讯录
  • youte-agent部署(windows)
  • Python实现点云法向量各种方向设定
  • Linnux IPC通信和RPC通信实现的方式
  • apache实现LAMP+apache(URL重定向)
  • MongoDB 与 GraphQL 结合:现代 API 开发新范式
  • k8s-临时容器学习
  • uni-app 根据用户不同身份显示不同的tabBar
  • ubuntu18.04安装PCL1.14
  • Ubuntu 系统下 Anaconda 完整安装与环境配置指南(附常见问题解决)
  • 网络链路分析笔记mtr/traceroute
  • 在 Ubuntu 系统中利用 conda 创建虚拟环境安装 sglang 大模型引擎的完整步骤、版本查看方法、启动指令及验证方式
  • 基带与射频的区别与联系
  • 《企业安全运营周报》模板 (极简实用版)​
  • opencv基于SIFT特征匹配的简单指纹识别系统实现
  • Node.js 操作 Elasticsearch (ES) 的指南
  • 使用tree命令导出文件夹/文件的目录树( Windows 和 macOS)
  • Spring缓存(二):解决缓存雪崩、击穿、穿透问题
  • LabVIEW加载 STL 模型至 3D 场景 源码见附件
  • Tessent_ijtag_ug——第 4 章 ICL 提取(2)