实现普通网址变为短网址
参考:
设计短链接(short URL)系统
短链接是怎么设计的?带你入门
一个go语言实现的短链接服务,包括生成短连接和解析短链接
java请求微博短链接API
Url短连接实现原理与方法
网页短链接实现原理探究
如何将一个长URL转换为一个短URL? 推荐参考
业务中一般在短信推广中,需要将长链接转为短链接,减少短信字符长度,节省短信费用。优化用户体验,便于复制和传播。一般情况下,会在短信内容中,推送活动链接,下载app的链接,查询信息链接等。
经过网上的查询和参考,大致两种方式。
第一种方式:调用百度、腾讯、微博提供的短连接生成api实现或者是第三方提供生成的短连接服务。
第二种方式:系统内自己实现。
百度的短网址需要企业级用户才能使用,具体参考(http://dwz.cn/) ,但是对于腾讯系的app中没有防封效果。
微博在2019年就停止了官方短网址api的调用,具体参考(https://open.weibo.com/wiki/2/short_url/shorten) ,短连接: t.cn
腾讯在微信公众号中,有一个长链接转短链接接口,具体参考(https://developers.weixin.qq.com/doc/offiaccount/Account_Management/URL_Shortener.html) ,短连接:w.url.cn/s/
具体实现,需要看业务场景。
思路:用户访问短链接,后台根据短链接,查找到对应的长链接,通过重定向(301,302)到长链接对应的网址。
推荐参考这篇文章:如何将一个长URL转换为一个短URL? 细节的实现方式,就直接看这篇文章就行。
或者直接看:https://www.zhihu.com/question/29270034/answer/46446911
具体代码参考:https://github.com/gengzi/codecopy.git/ 中 fun.gengzi.codecopy.business.shorturl 下的代码
数据库:resources/db/shorturl.sql
jmeter测试脚本:resources/shorturltest.jmx
总结一下思路:分为两个接口,一个是长链接转短链接的接口,一个是短链接重定向到对应长链接的接口。
public interface ShortUrlGeneratorService {
/**
* 返回短链接
*
* @param longUrl 普通链接
* @return 短链接
*/
String generatorShortUrl(String longUrl);
/**
* 返回长链接
* @param shortUrl 短链接
* @return 长链接
*/
String getLongUrl(String shortUrl);
}
长链接转短链接接口:
/**
* 返回短链接
* // 判断当前长连接能否在redis 查找到,查找到直接返回短链接,并更新这个key value 的过期时间为1小时
* // 不是,调用redis逻辑发号器
* // 返回号码,作为数据库的主键,检测主键是否冲突,冲突重新尝试拿新的号码(也可以不验证是否主键冲突,只要能保证发号器发的号码是唯一的)
* // 将长连接和号码绑定,将10进制的号码,转换为62进制
* // 组拼短链接,设置超时时间
* // 存入数据库
* // 存入redis,key value 的形式,key 62进制id ,对应一个长连接,如果数量太多,可以设置一个失效时间(比如 三天),防止redis中缓存太多
* // 再次存入reids, key value 的形式,长连接,对应一个 短链接的62进制,设置失效时间是 1 小时,当同一个长链接再来,就可以直接从redis中返回
* // 返回
*
* @param longUrl 普通链接
* @return
*/
@Transactional
@Override
public String generatorShortUrl(String longUrl) {
logger.info("-- longurl to shorturl start --");
logger.info("param longurl : {}", longUrl);
// 判断当前连接不能为null 或者 " "
if (StringUtils.isNoneBlank(longUrl)) {
boolean isExist = redisUtil.hasKey(longUrl);
if (isExist) {
String shortUrl = (String) redisUtil.get(longUrl);
redisUtil.expire(longUrl, ShortUrlConstant.UPDATETIMEHOUR, TimeUnit.HOURS);
logger.info("redis get shorturl success, return shorturl : {}", linkUrlPre + shortUrl);
return linkUrlPre + shortUrl;
} else {
long number = redisUtil.getRedisSequence();
String str62 = BaseConversionUtils.to62RadixString(number);
String genShortUrl = linkUrlPre + str62;
Shorturl shorturl = new Shorturl();
shorturl.setId(number);
shorturl.setLongurl(longUrl);
shorturl.setShorturl(genShortUrl);
shorturl.setIsoverdue(Integer.valueOf(ShortUrlConstant.ISOVERDUE));
shorturl.setTermtime(new Date());
shorturl.setCreatetime(new Date());
shorturl.setUpdatetime(new Date());
shortUrlGeneratorDao.save(shorturl);
// 将62进制跟长链接保存到session,这里没有保存短链接,因为前缀没必要存入缓存
redisUtil.set(str62, longUrl);
redisUtil.set(longUrl, str62, 1, TimeUnit.HOURS);
logger.info("insert shorturl success , return shorturl : {} ", genShortUrl);
return genShortUrl;
}
}
return "";
}
短链接跳转长链接:
controller:
@ApiOperation(value = "短链接跳转服务", notes = "短链接跳转服务")
@ApiImplicitParams({
@ApiImplicitParam(name = "shorturl", value = "短链接", required = true)})
@GetMapping("/u/{shorturl}")
public String redirectUrl(@PathVariable("shorturl") String shorturl) {
logger.info("redirectUrl start {} ", System.currentTimeMillis());
String longUrl = shortUrlGeneratorService.getLongUrl(shorturl);
return "redirect:" + longUrl;
}
service:
/**
* 返回长链接
* // 判断当前连接能否在redis 查找到,查找到直接返回长连接
* // 将62进制,转为10进制
* // 判断返回长连接是无,则在数据库中查找
*
* @param shortUrl 短链接
* @return 长链接
*/
@Override
public String getLongUrl(String shortUrl) {
String longUrl = (String) redisUtil.get(shortUrl);
if (StringUtils.isNoneBlank(longUrl)) {
return longUrl;
}
long shortUrlId = BaseConversionUtils.radixString(shortUrl);
Shorturl shorturl = shortUrlGeneratorDao.getOne(shortUrlId);
if (shorturl != null) {
return shorturl.getLongurl();
}
return "";
}
增加缓存层,使用nosql数据库,将经常频繁转换的长链接和短链接存入,并设置过期时间,每转换一次更新一次时间。(有了)
增加链接时效性的校验,判断过期,则跳转到提示页面,提示活动已过期
增加接口认证,对于授权的用户,才能调用长链接转短链接服务,记录调用次数,记录ip,限制调用次数,防止恶意用户刷接口
短链接重定向302,记录一下行为数据,用户的ip,使用的终端,地区等等,这些数据用于优化以后的业务场景
短链接的业务区分,比如 http://域名/s/uA3x 这个通过 s 标识特定的一种场景
对于分享型的链接,可以将分享人信息也脱敏处理后,追加短链接的后面,来进行一些业务统计。
之前使用的发号器(就是生成数据库id的策略),我们使用了单redis自增序列发号器,分段redis自增序列发号器。这些在分布式和高并发下并不适用,所以可以使用,分布式发号器来生成id,可以参考 雪花算法,或者是开源的全局id生成器。
对于长链接,短链接的存储,如果数据量比较大,对于单表的查询和更新都是缓慢的。读写分离,分库分表,或者采用别的方式存储
//todo 等我实践到这里,再看需要什么方案
10进制与62进制互转
public class BaseConversionUtils {
static final char[] DIGITS =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
// 转62进制
public static String to62RadixString(long seq) {
StringBuilder sBuilder = new StringBuilder();
while (true) {
int remainder = (int) (seq % 62);
sBuilder.append(DIGITS[remainder]);
seq = seq / 62;
if (seq == 0) {
break;
}
}
return sBuilder.reverse().toString();
}
// 转10进制
public static long radixString(String str) {
long sum = 0L;
int len = str.length();
for (int i = 0; i < len; i++) {
sum += indexDigits(str.charAt(len - i - 1)) * Math.pow((double) 62, (double) i);
}
return sum;
}
private static int indexDigits(char ch) {
for (int i = 0; i < DIGITS.length; i++) {
if (ch == DIGITS[i]) {
return i;
}
}
return -1;
}
}