短链接服务实践


目标

业务场景

业务中一般在短信推广中,需要将长链接转为短链接,减少短信字符长度,节省短信费用。优化用户体验,便于复制和传播。一般情况下,会在短信内容中,推送活动链接,下载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;
        }
    
    }
    

    在这里插入图片描述

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