短连接设计说明书 v1
功能概述
短连接服务系统Url Shorten Service, 简称USS, 主要提供长连接缩短为短连接、还原短连接等功能
数据库设计
CREATE TABLE `uss_urls` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`code` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '短链接的串码',
`originUrl` text COLLATE utf8_unicode_ci,
`sha1` varchar(50) CHARACTER SET utf8 NOT NULL,
`seqId` bigint(20) DEFAULT NULL,
`createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expiredAt` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `USS_CODE_UNIQUE` (`code`),
UNIQUE KEY `USS_SEQID` (`seqId`),
KEY `USS_EXPIRED_AT` (`expiredAt`,`createdAt`),
KEY `USS_SHA1` (`sha1`)
) ENGINE=InnoDB AUTO_INCREMENT=173293 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
短连接算法:
- 将"原始链接(长链接)+ key(随机字符串,防止算法泄漏)"MD5后得到32位的一个字符串A
- 将上面的A字符串分为4段处理, 每段的长度为8 , 比如四段分别为 M、N、O、P
- 将M字符串当作一个16进制格式的数字来处理, 将其转换为一个Long类型。 比如转换为L
- 此时L的二进制有效长度为32位, 需要将前面两位去掉,留下30位 , 可以 & 0x3fffffff 进行位与运算得到想要的结果。(30位才能转换62进制,否则超出)
- 此时L的二进制有效长度为30位, 分为6段处理, 每段的长度为5位
- 依次取出L的每一段(5位),进行位操作 & 0x0000003D 得到一个 <= 61的数字,来当做index
- 根据index 去预定义的62进制字符表里面去取一个字符, 最后能取出6个字符,然后拼装这6个字符成为一个6位字符串,作为短链接码
拿出第②步剩余的三段,重复3-7 步 - 这样总共可以获得 4 个6位字符串,取里面的任意一个就可作为这个长链接的短网址
- 串码添加校验位checksum,用于简单校验。所以总共7位码
接口设计 : 主要设计三个接口
Map<String, Object> shorten(MMURL mmurl);
生成短连接码 见 图1.png
- 第一步验证长度 大于40小于5000
- 第二步拿到环境域名
- 第三部 使用SHA-1算法加密 16进制
SHA-1算法:
public static String encode(String originTxt, String encodingAlgorithm, String charsetEncoding) {
if (originTxt == null) {
return null;
} else {
try {
MessageDigest messageDigest = MessageDigest.getInstance(encodingAlgorithm);
if (StringUtil.isNotBlank(charsetEncoding)) {
messageDigest.update(originTxt.getBytes(charsetEncoding));
} else {
messageDigest.update(originTxt.getBytes("UTF-8"));
}
byte[] digest = messageDigest.digest();
return HexUtil.bytesToHex(digest);
} catch (NoSuchAlgorithmException var5) {
throw new SecurityException(var5);
} catch (UnsupportedEncodingException var6) {
throw new RuntimeException(var6);
}
}
}
- 第四部查询当前html是否转换过短连接(读取redis缓存)
- 第五步计算seqid 主要依赖于redis incr操作实现
部分代码实现如下:
/**
* 同步方法, 获取redis的sequence累加值,如果redis无数据,先从DB里面获取预热。
* @return
*/
public Long incrSeqId() {
long redisSeqId = getStr2long(UssConstants.REDIS_USS_SEQ_KEY);
if (redisSeqId < UssConstants.INIT_SEQ_ID) {
synchronized (INCR_LOCK) {
redisSeqId = getStr2long(UssConstants.REDIS_USS_SEQ_KEY);
if (redisSeqId < UssConstants.INIT_SEQ_ID) {
Long dbSeqId = ussService.queryMaxSeqId();
//DB存在就以DB为启动基数, DB不存在就是初始值1982
final long seqId = (dbSeqId != null && dbSeqId > UssConstants.INIT_SEQ_ID) ?
dbSeqId:
UssConstants.INIT_SEQ_ID;
//设置永久的Key
String oldSeqId = stringOpr.getAndSet(UssConstants.REDIS_USS_SEQ_KEY,
String.valueOf(seqId));
//set之前Redis中已经存在一个比较大的值,说明集群其他节点并发插入了
//减少Lock复杂度的实现,直接在当前的基础上补上(差值+500),减少冲突的可能性即可。
long oldSeqIdLng = (oldSeqId != null ? Long.parseLong(oldSeqId) : 0);
if (oldSeqIdLng > seqId) {
longOpr.increment(UssConstants.REDIS_USS_SEQ_KEY,
oldSeqIdLng - seqId + 500);
}
stringOpr.getOperations().persist(UssConstants.REDIS_USS_SEQ_KEY);
}
}
}
//用随机数来自增
return longOpr.increment(UssConstants.REDIS_USS_SEQ_KEY,
RandomUtils.nextInt(1, 10));
}
- 第六步生成短码code,短码code分为 分为4到5个步骤个部分 第一位随机混淆,第二位字符表示序列号转成62进制后的长度(用于反向转换),第三位开始是自增长序列,倒数第二位随机混淆,最后一位校验位
代码:
/**
* 自增序列基础上的缩短算法
* 第一位 第二位 中间位数 倒数第二位 最后一位
* 随机混淆字符 62进制序号的长度 自增序号的62进制 随机混淆字符 校验位
*
* @param seqId
* @return
*/
public static String shorten(long seqId) {
if (seqId < 1L) {
throw new RuntimeException("SeqId is too small!");
}
String seqIdStr62 = Base62Utils.fromBase10(seqId);
int bits = seqIdStr62.length();
StringBuffer shortenCode = new StringBuffer();
//第一位随机混淆
shortenCode.append(DIGITS[RandomUtils.nextInt(0, 62)]);
//第二位字符表示序列号转成62进制后的长度(用于反向转换)
shortenCode.append(Base62Utils.fromBase10(bits));
//第三位开始是自增长序列
shortenCode.append(seqIdStr62);
//补充混淆字段保证短链7位起
if (bits < 3) {
shortenCode.append(RandomStringUtils.randomAlphanumeric(3 - bits));
}
//倒数第二位随机混淆
shortenCode.append(DIGITS[RandomUtils.nextInt(0, 62)]);
//最后一位校验位
return LuhnUtil.mmGenCodeWithCheckSum(shortenCode.toString());
}
- 生成短连接 保存数据库,redis
Map<String, Object> restore(String shortenCode);
还原短链接 说明详见图2.png
- 第一步校验短码是否匹配 主要是使用了
lunh算法实现原理
- 从校验位开始,从右往左,偶数位乘2,然后将两位数字的个位与十位相加;
- 计算所有数字的和(67);
- 乘以9(603);
- 取其个位数字(3),得到校验位。
lunh算法代码:
public static boolean luhnCheck(String code) {
if (StringUtil.isBlank(code)) {
return false;
} else {
int[] checkArr = convertStrToInArr(code);
int sum;
for(sum = 1; sum < checkArr.length; sum += 2) {
checkArr[sum] <<= 1;
checkArr[sum] = checkArr[sum] / 10 + checkArr[sum] % 10;
}
sum = 0;
for(int i = 0; i < checkArr.length; ++i) {
sum += checkArr[i];
}
return sum % 10 == 0;
}
}
- 第二步读取redis缓存 读取不到从数据库读取 redis不存在记录一天缓存
Map<String, Object> revoke(String shortenCode);
销毁短连接 详见图3.png
- 第一步 判断短码是否redis不存在记录 存在直接返回 不存在直接di
- 第二步 修改数据库的短码过期时间 删除redis缓存