短鏈接服務實踐


目標

業務場景

業務中一般在短信推廣中,需要將長鏈接轉爲短鏈接,減少短信字符長度,節省短信費用。優化用戶體驗,便於複製和傳播。一般情況下,會在短信內容中,推送活動鏈接,下載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;
        }
    
    }
    

    在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章