目標
-
實現普通網址變爲短網址
如何將一個長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; } }