-
Notifications
You must be signed in to change notification settings - Fork 5
coderdream_20220506
CoderDream edited this page May 6, 2022
·
1 revision
package com.coderdream.utils;
/**
* @author :CoderDream
* @date :Created in 2022/4/28 20:10
* @description:
* @modified By:CoderDream
* @version: $
*/
public class Constants {
/**
* 62个字符
*/
public static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 布隆过滤器的容量 100000000
*/
public static final Long BLOOM_FILTER_INSERTION = 100000000L;
/**
* 期望的误判率
*/
public static final double BLOOM_FILTER_FPP = 0.01;
/**
* Guava Cache 并发级别为8,并发级别是指可以同时写缓存的线程数
*/
public static final Integer CONCURRENCY_LEVEL = 8;
/**
* Guava Cache 缓存容器的初始容量为 10
*/
public static final Integer INITIAL_CAPACITY = 10;
/**
* Guava Cache 设置缓存最大容量为 100000000 Long 的最大值 9,223,372,036,854,775,807 6 位 62 进制数的可能的组合 56,800,235,584
*/
public static final Long MAXIMUM_SIZE = 100000000L;
/**
* Guava Cache 设置缓存的水位
*/
public static final Float WATER_LEVEL = 0.95F;
}
package com.coderdream.generator;
public interface CodeGenerator {
/**
* 产生的code
*
* @return
*/
String generateCode();
}
package com.coderdream.helper;
import com.coderdream.utils.CommonUtil;
import com.coderdream.utils.Constants;
import org.springframework.stereotype.Component;
/**
* @author :CoderDream
* @date :Created in 2022/4/25 20:12
* @description:
* @modified By:CoderDream
* @version: $
*/
@Component
public class ShortLinkHelper {
/**
* 生成短链码
*
* @param param
* @return
*/
public String createShortLinkCode(String param) {
if(param == null || "".equals(param.trim())) {
return "";
}
long murmurhash = CommonUtil.murmurHash32(param);
//进制转换
return encodeToBase62(murmurhash);
}
/**
* 10进制转62进制
*
* @param num
* @return
*/
private String encodeToBase62(long num) {
// StringBuffer线程安全,StringBuilder线程不安全
StringBuffer sb = new StringBuffer();
do {
int i = (int) (num % 62);
sb.append(Constants.CHARS.charAt(i));
num = num / 62;
} while (num > 0);
return sb.reverse().toString();
}
}
package com.coderdream.helper;
import com.coderdream.utils.Config;
import com.coderdream.utils.Constants;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author :CoderDream
* @date :Created in 2022/4/26 20:08
* @description:
* @modified By:CoderDream
* @version: $
*/
@Service
@Slf4j
public class GuavaCacheHelper {
@Resource
private Config config;
private Cache<Object, Object> cache;
@PostConstruct
public void init() {
cache = CacheBuilder.newBuilder()
// 设置并发级别为8,并发级别是指可以同时写缓存的线程数,此处最好和cpu数一致(Runtime.getRuntime().availableProcessors())
.concurrencyLevel(Constants.CONCURRENCY_LEVEL)
// 设置缓存容器的初始容量为10
.initialCapacity(Constants.INITIAL_CAPACITY)
// 设置缓存最大容量为100,超过100之后就会按照LRU最近最少使用算法来移除缓存项,生产环境可设置为 100000000
.maximumSize(Constants.MAXIMUM_SIZE)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(config.EXPIRE_SEC, TimeUnit.SECONDS)
// 设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
// 只阻塞当前数据加载线程,其他线程返回旧值
//.refreshAfterWrite(13, TimeUnit.SECONDS)
// 设置缓存的移除通知
.removalListener(notification -> {
log.error(notification.getKey() + " " + notification.getValue() + " 被移除,原因:" + notification.getCause());
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build();
}
/**
* 获取缓存
*/
public Object get(String key) {
return StringUtils.isEmpty(key) ? null : cache.getIfPresent(key);
}
/**
* 获取缓存大小
*/
public long size() {
return cache.size();
}
/**
* 放入缓存
*/
public void put(String key, Object value) {
if (StringUtils.isNotEmpty(key) && value != null) {
cache.put(key, value);
} else {
return;
}
}
}
package com.coderdream.generator;
import com.coderdream.service.LinkService;
import com.coderdream.utils.Constants;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
public class StringGeneratorTest {
//
// @Resource
// private StringGenerator stringGenerator;
@Test
public void testGetShortLink() {
StringGenerator stringGenerator = new StringGenerator();
for (int i = 0; i < 64; i++) {
System.out.println(stringGenerator.generateCode());
}
// String shortLink = linkService.getShortLink("http://www.baidu.com/jLFYW7ZlML-344410");
// Assert.assertNotNull(shortLink);
// Assert.assertEquals("http://www.baidu.com/jLFYW7ZlML-344410", linkService.getLongLink(shortLink));
}
}
package com.coderdream.helper;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShortLinkHelperTest {
@Resource
private ShortLinkHelper shortLinkHelper;
@Test
public void testCreateShortLinkCode() {
assertEquals("",shortLinkHelper.createShortLinkCode(null));
assertEquals("",shortLinkHelper.createShortLinkCode(""));
assertEquals("1BWWGy", shortLinkHelper.createShortLinkCode("http://www.baidu.com/jLFYW7ZlML-344410"));
}
}
package com.coderdream.service.impl;
import com.coderdream.bean.ShortLinkBean;
import com.coderdream.generator.RandomStringGenerator;
import com.coderdream.generator.StringGenerator;
import com.coderdream.helper.BloomFilterHelper;
import com.coderdream.helper.FileOperateHelper;
import com.coderdream.helper.GeneratorHelper;
import com.coderdream.helper.GuavaCacheHelper;
import com.coderdream.service.LinkService;
import com.coderdream.utils.Config;
import com.coderdream.utils.Constants;
import com.coderdream.utils.DuplicatedEnum;
import com.coderdream.helper.ShortLinkHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
@Service
@Slf4j
public class LinkServiceImpl implements LinkService {
@Resource
private Config config;
@Resource
private FileOperateHelper fleOperateHelper;
@Resource
private ShortLinkHelper shortLinkHelper;
@Resource
private GuavaCacheHelper guavaCacheHelper;
@Resource
private BloomFilterHelper bloomFilterHelper;
@Resource
private GeneratorHelper generatorHelper;
/**
* 号码生成器列表
*/
@Resource
private RandomStringGenerator randomStringGenerator;
private String generateCode;
private Set<String> hashCodeSet;
@PostConstruct
public void init() {
//stringGeneratorList = generatorHelper.createStringGenerator();
generateCode = randomStringGenerator.generateCode();
hashCodeSet = new HashSet<>();
// machineId = Long.parseLong(fileOperateAbility.readFile("machineId"));
}
@Override
public String getShortLink(String longLink) {
// 校验longLink
if (longLink == null || "".equals(longLink)) {
// 记录日志
log.error("入参错误,不能为空:" + longLink);
return "";
}
// 获取机器ID,这里写在文件中,后期通过ZooKeeper管理
String machineId = fleOperateHelper.readFile("machineId");
// 生成短链接
String code = shortLinkHelper.createShortLinkCode(longLink);
// 记录日志
//log.error("缓存大小:" + guavaCacheHelper.size());
Long guavaCacheSize = guavaCacheHelper.size();
Long initSize = Constants.MAXIMUM_SIZE;
Float rate = Float.valueOf(guavaCacheSize) / Float.valueOf(initSize);
// 记录日志
// log.error("目前缓存水位:" + rate);
// if (Constants.WATER_LEVEL < rate) {
generateCode = randomStringGenerator.generateCode();
// }
// 机器ID作为前缀
code = machineId + generateCode + code;
// 创建短链接Bean
ShortLinkBean shortLinkBean = new ShortLinkBean();
shortLinkBean.setShortLink(code);
shortLinkBean.setLongLink(longLink);
shortLinkBean.setExpireTime(System.currentTimeMillis() + config.EXPIRE_SEC * 1000);
// 如果存在(可能误判)
if (bloomFilterHelper.mightContain(code)) {
// 从缓存中取对象
ShortLinkBean oldShortLinkBean = (ShortLinkBean) guavaCacheHelper.get(code);
// 如果不存在误判为存在,则直接将新的数据写入缓存中
if (oldShortLinkBean == null) {
// 把短链接放入Guava缓存中
guavaCacheHelper.put(code, shortLinkBean);
// 把短链接放入布隆过滤器
bloomFilterHelper.put(code);
// 记录日志
log.warn("布隆过滤器误判: new code: " + code + " ; new link: " + longLink);
}
// 如果确实存在
else {
String oldLongLink = oldShortLinkBean.getLongLink();
// 判断是否Hash冲突了(code相同,长链接url不同)
if (code.equals(oldShortLinkBean.getShortLink()) && !longLink.equals(oldLongLink)) {
String totalCode = code;
// 记录日志
log.warn("Hash冲突, old and new code: " + code + "; old link: " + oldLongLink + " ; new link: "
+ longLink);
// 构造新code、新link
// code加上枚举前缀后再取Hash,生成新的短链接
code = machineId + generateCode + shortLinkHelper.createShortLinkCode(
DuplicatedEnum.DUPLICATED.getKey() + "_" + code);
totalCode = totalCode + "_" + code;
log.error("TotalCode:" + totalCode);
hashCodeSet.add(totalCode);
// 长链接加上前缀
String newLongLink = DuplicatedEnum.DUPLICATED.getKey() + "_" + longLink;
log.error("Hash冲突解决: new code: " + code + "; old link: " + oldShortLinkBean.getLongLink()
+ " ; new link: " + newLongLink);
// 设置新的短链接
shortLinkBean.setShortLink(code);
// 设置新的长链接
shortLinkBean.setLongLink(newLongLink);
// 把短链接放入Guava缓存中
guavaCacheHelper.put(code, shortLinkBean);
// 把短链接放入布隆过滤器
bloomFilterHelper.put(code);
}
// 未冲突,已存在数据,不做处理,既不放到缓存中,也不放到过滤器中
else {
// 记录日志
log.info("已存在: code " + code + " ; longLink: " + longLink);
}
}
}
// 通过布隆过滤器判断:如果不存在(100%正确),则直接放入缓存中
else {
// 把短链接放入Guava缓存中
guavaCacheHelper.put(code, shortLinkBean);
// 把短链接放入布隆过滤器
bloomFilterHelper.put(code);
}
// 记录日志
// log.info("Hash冲突集合大小: " + hashCodeSet.size());
// 将短链接返回给调用方
return code;
}
@Override
public String getLongLink(String shortLink) {
// 校验
if (shortLink == null || "".equals(shortLink)) {
// 记录日志
log.error("入参错误,不能为空:" + shortLink);
return "";
}
// 从缓存中获取对象
ShortLinkBean shortLinkBean = (ShortLinkBean) guavaCacheHelper.get(shortLink);
// 如果不存在,则记日志,然后返回空
if (shortLinkBean == null) {
log.warn("缓存中不存在 " + shortLink + " 对应的长链接");
return "";
}
// 如果存在,处理长链接
else {
// 取出长链接
String longLink = shortLinkBean.getLongLink();
// 如果不存在Hash冲突标记,则直接返回长链接
if (!longLink.startsWith(DuplicatedEnum.DUPLICATED.getKey())) {
return longLink;
}
// 否则去掉冲突标记后再返回
else {
log.warn("去掉冲突标记后再返回:" + longLink);
longLink = longLink.replace(DuplicatedEnum.DUPLICATED.getKey() + "_", "");
}
// 将长链接返回给调用方
return longLink;
}
}
}
package com.coderdream.helper;
import com.coderdream.generator.StringGenerator;
import com.coderdream.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author XY
* @Description
* @createTime 2021年12月17日 21:21:00
*/
@Service
@Slf4j
public class GeneratorHelper {
/**
* 获取队列发号器
*
*/
public List<StringGenerator> createStringGenerator() {
List<StringGenerator> list = new ArrayList<>();
int n = Constants.CHARS.toCharArray().length;
for (Long i = 0L; i < n; i++) {
list.add(new StringGenerator());
log.info("[createStringGenerator],队列发号器创建,id={}", i);
}
return list;
}
}
package com.coderdream.generator;
import com.coderdream.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 功能描述
*
* @since 2022-05-05
*/
@Slf4j
@Service
public class StringGenerator implements CodeGenerator {
private ArrayBlockingQueue<String> queue;
@Override
public String generateCode() {
return queue.poll();
}
public StringGenerator() {
Integer queueSizeConfig = Constants.CHARS.toCharArray().length;
queue = new ArrayBlockingQueue<>(queueSizeConfig);
List<String> charList = new ArrayList<>();
for (int i = 0; i < queueSizeConfig; i++) {
Character character = Constants.CHARS.toCharArray()[i];
charList.add(character.toString());
}
//后台线程
Thread thread = new Thread(() -> {
int index = 0;
while (index < queueSizeConfig) {
// if (this.low >= this.highMax) {
// log.error("已用尽号码,停止后台线程,id={},low={},high={},step={},highMax={}", id, low, high, step, highMax);
// return;
// }
// //如果低水位达到高水位,高水位上移
// if (low >= high) {
// high += step;
// //高水位不可越过最高水位
// if (high >= highMax) {
// high = highMax;
// }
// //todo 持久化记录high
// }
// try {
// //采用阻塞方式放入元素
// queue.put(charList.get(index));
// } catch (InterruptedException e) {
// log.info("[CacheQueueNumberGenerator] daemon thread interrupted", e);
// }
//低水位移动
index++;
//todo 可以加提前预警,比如low>highMax*0.8
}
});
thread.setDaemon(true);
thread.start();
int index = 0;
while (index < queueSizeConfig) {
// if (this.low >= this.highMax) {
// log.error("已用尽号码,停止后台线程,id={},low={},high={},step={},highMax={}", id, low, high, step, highMax);
// return;
// }
// //如果低水位达到高水位,高水位上移
// if (low >= high) {
// high += step;
// //高水位不可越过最高水位
// if (high >= highMax) {
// high = highMax;
// }
// //todo 持久化记录high
// }
try {
//采用阻塞方式放入元素
queue.put(charList.get(index));
} catch (InterruptedException e) {
log.info("[CacheQueueNumberGenerator] daemon thread interrupted", e);
}
//低水位移动
index++;
//todo 可以加提前预警,比如low>highMax*0.8
}
System.out.println(queue.size());
}
}
package com.coderdream.generator;
import com.coderdream.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
/**
* 功能描述
*
* @since 2022-05-05
*/
@Slf4j
@Service
public class RandomStringGenerator {
private List<String> stringList = new ArrayList<>();
public String generateCode() {
Random random = new Random();
int index = random.nextInt(Constants.CHARS.toCharArray().length);
return stringList.get(index) ;
}
public RandomStringGenerator() {
Integer queueSizeConfig = Constants.CHARS.toCharArray().length;
for (int i = 0; i < queueSizeConfig; i++) {
Character character = Constants.CHARS.toCharArray()[i];
stringList.add(character.toString());
}
System.out.println(stringList.size());
}
}