作为9年经验的开发,现在的面试据说已经不仅仅是八股文了,本身到这个阶段对我而言,单纯的技术已经没有意义,还是要为业务去选择合适的技术,摘录一些我认为高频或者实用的场景设计题;【只开放7天哦】
个人的微信公众号:印象人生;【后面打算同步文章到微信公众号,欢迎来踩】
“站内信”有两个基本功能:
点到点的消息传送。用户给用户发送站内信,管理员给用户发送站内信。
点到面的消息传送。管理员给用户(指定满足某一条件的用户群)群发消息
这两个功能实现起来也很简单:
只需要设计一个消息内容表和一个用户通知表,当创建一条系统通知后,数据插入到消息内容表。消息内容包含了发送渠道,根据发送渠道决定后续动作。
如果是站内渠道,在插入消息内容后异步的插入记录到用户通知表。
这个方案看起来没什么问题,但实际上,我们把所有用户通知的消息全部放在一个表里面,如果有 10W 个用户,那么同样的消息需要存储 10W 条。
很明显,会带来两个问题:
随着用户量的增加,发送一次消息需要插入到数据库中的数据量会越来越大,导致耗时会越来越长;
用户通知表的数据量会非常大,对未读消息的查询效率会严重下降;
所以上面这种方案很明显行不通,要解决这两个问题,我有两个参考解决思路。
分布式ID是确保系统中的每个数据项都具有全局唯一的标识,满足系统需求并提高开发效率的关键。
在分布式系统中,数据可能分布在多个服务器、数据库或其他存储设备上。为了确保系统中的每个数据项都具有全局唯一的标识,需要使用分布式ID。随着系统的数据量越来越大,单机MySQL已经无法满足需求,需要进行分库分表。数据库的自增主键已经无法满足生成的主键唯一性,此时就需要生成分布式ID。使用分布式ID可以解决数据分布和唯一标识的问题,满足系统需求并提高开发效率。
分布式ID需要满足的要求包括【重要】:
UUID、数据库自增、号段模式、Redis实现、雪花算法(SnowFlake)、百度Uidgenerator、美团Leaf、滴滴TinyID等。
每种方案都有其优缺点,选择最适合的方案需要结合具体的业务场景和需求。
最终分布式ID肯定是要做DB存储的,这就意味着要满足索引或主键的一些要求
分布式ID最终是要落库的,InnoDB存储引擎使用聚簇索引来组织表中的数据。
聚簇索引定义了数据在磁盘上的物理存储顺序。数据实际上是存储在索引的叶子节点上的。这意味着,当你通过主键查询数据时,InnoDB可以直接在索引中找到相应的数据,而无需进行额外的磁盘I/O操作。这就是所谓的“覆盖索引”查询,它可以大大提高查询性能。
然而,聚簇索引的一个潜在缺点是它可能导致数据页分裂。
那什么是数据分页呢? lnnoDB 不是按行来操作数据的,它可操作的最小单位是页,页加载进内存后才会通过扫描页来获取行记录比如查询 id=1,是获取 1所在的数据页,加载进内存后取出1这一行。
页的默认大小为16KB,64个连续的数据页称为一个extent(区),64个页组成一个区,所以区的大小为1MB(16*64=1024),连续的256个数据区称为一组数据区。
数据页分裂是一个相对昂贵的操作,因为它涉及到数据的移动和可能的磁盘I/O操作。
当一个数据页中的数据行太多放不下的时候就会生成一个新的数据页来存储, 同时使用双向链表来相连; 使用索引时,一个最基本的条件是后面数据中的数据行的主键值要大于前一个数据页中数据行的主键值。
如果我们设置的主键是乱序的, 就有可能会导致数据页中的主键值大小不能满足索引使用条件。所以就会要求主键必须有序。
如果值有序,但是插入的数据不是递增的,此时就会产生页分裂, 如下图的数据页:
可以发现后面数据页里的主键值比前一个数据页的主键值小, 里面的数据就会进行数据挪动,那这就是我们所说的页分裂。
通过页分裂,我们只要将主键为2的数据行与主键值为4的数据行互相挪动一下位置,就可以保证后面一个数据页的主键值比前一个数据页中的主键值大了,
为了更清晰地理解页分裂,我们可以将其步骤概括为:
检查空间:当InnoDB尝试插入新的数据时,它首先会检查当前数据页是否有足够的空间来容纳新数据。
分裂决策:如果当前页没有足够的空间,InnoDB就会决定进行页分裂。它会创建一个新的数据页,并将原数据页中的一部分数据(通常是中位数附近的数据)移动到新页中,以确保新插入的数据可以放在合适的位置。
数据移动:实际的数据移动过程涉及将原数据页中的一部分行复制到新页中,并更新相关的索引和指针以反映这种变化。这可能涉及到多个数据页的调整,以确保数据的连续性和索引的正确性。
更新链接:InnoDB会更新数据页之间的双向链表指针,以确保分裂后的数据页仍然按照正确的顺序链接在一起。同时,它也会更新索引结构以反映新数据页的存在和位置。
插入新数据:一旦页分裂完成,InnoDB就可以在新的位置插入新数据了。这通常是在分裂后留下的空间中进行的。
需要注意的是,页分裂不仅发生在插入操作中。当更新操作导致行的大小增加,使得当前页无法容纳时,也可能发生页分裂。同样地,删除操作可能导致页的合并,以释放空间并提高存储效率。
为了减少页分裂的频率和提高写入性能,可以采取以下策略:
所以,其结论就是主键值最好是有序的, 不仅可以不用页分裂,还能充分使用到索引。否则必须进行页分裂来保证索引的使用。
UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID的标准型式包含32个16进制数字,标准格式为:8-4-4-4-12,总长度为36个字符(包括4个-号)
优点
(1)技术实现简单,一行代码即可。
(2)本地即可生成,出错率低。
(3)ID生成性能高。
缺点
(1)无序,影响数据库的数据写入性能。
(2)存储成本高,就算去掉4个“-”,长度也是32。
(3)可读性差。
场景
分布式链路追踪ID
千万不要忽视索引重建带来的问题,看下我们公司之前的表,200w+的核心表,单字段索引+联合索引有14个,造成的结果是索引物理存储几乎是数据的5倍,如果采用的分布式id做索引或者主键,其带来的索引重建,随机IO和频繁的页分裂,肯定是不小
基于数据库的auto_increment自增ID完全可以充当分布式ID:
优点
缺点
场景
单库低业务量场景
CREATE TABLE id_generator (
`id` int(10) NOT NULL,
`max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(20) NOT NULL COMMENT '号段的步长',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
`version` int(20) NOT NULL COMMENT '版本号乐观锁',
PRIMARY KEY (`id`)
)
等ID都用了,再去数据库获取,然后更改最大值
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
优点
缺点
场景
优缺点明显,偏向业务复杂的场景,感觉使用这个模式的公司,都是在业务发展前期设计的,后续打了很多的补丁,使用上和运营依旧会遇见很多不可控的问题;
Redis分布式ID实现主要是通过提供像INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性;
这种实现方式,如果并发请求量上来后,就需要集群,不过集群后,又要和传统数据库一样,设置分段和步长;
优点
缺点
场景
还真没见有人用过这个做分布式环境的发号器
位置 | 取值 | 说明 |
---|---|---|
1bit | 固定0 | 代表正整数 |
41bit时间戳 | 2^41-1 | 1个数=1毫秒=>69年 |
10bit机器id | 2^10=1024 | 机房+机器一共10位=>1024个节点 |
12bit序列号 | 2^12-1=4095 | 单节点1毫秒可产生4095个ID序号 |
速记口诀:一是一,要灵要饿【141,1012】
从上面的组成可以看出来,雪花算法基础逻辑上可以保证以下两个特点:
在了解其组成结构以后,我结合网上的资料,改造了一个工具类:
@Slf4j
@Data
public class DistributeIdGenerator {
/**
* workerId部分1:数据中心占用的位数 原默认5
*/
private int dataCenterBitNum = 0;// 此处我设置的0
/**
* workerId部分2:机器码占用的位数,原默认5
*/
private int machineBitNum;
/**
* 随机数的占位
*/
private int sequenceBitNum;
/**
* 起始的时间戳
*/
private static final long TIMESTAMP = 1716981729000L;
/**
* 用于进行末尾sequence随机数产生
*/
private static final Random RANDOM = new Random();
/**
* datacenter编号 我们不用,默认值为0
*/
private long datacenterId = 0;
/**
* 机器编号
*/
private volatile long machineId = -1;
/**
* 上次生成id的最新时间戳
*/
private long lastStamp = -1L;
/**
* 时间戳偏移位数:机房位+机器位+序列位
*/
private int timestampLeftShift;
/**
* datacenter偏移位数:机器位+序列位
*/
private int datacenterIdShift;
/**
* 机器id偏移位数:序列位
*/
private int machineIdShift;
/**
* 每秒产生的最大序列值:~(-1L << sequenceBitNum)
* 假如sequenceBitNum=12
* 第一步:11111111111111111111111111111111(32位)左移12位得到11111111111111111111000000000000
* 第二部:使用按位非操作 ~,即取反,每个 1 变成 0,每个 0 变成 1。所以,上面的二进制串取反后变为:00000000000000000000111111111111
* 第三步:二进制转10进制,其实就是2的12次方-1的值,即4095
*/
private Long maxSequenceValue;
/**
* 根据上面的规则初始化偏移量
*/
private void initShiftOffset() {
timestampLeftShift = this.dataCenterBitNum + this.machineBitNum + this.sequenceBitNum;
datacenterIdShift = this.machineBitNum + this.sequenceBitNum;
machineIdShift = this.machineBitNum;
maxSequenceValue = ~(-1L << sequenceBitNum);
}
/**
* 私有化构造,禁止new
*/
private DistributeIdGenerator() {
}
/**
* 产生下一个ID
*/
public synchronized long nextId() {
long currentStamp = System.currentTimeMillis();
if (currentStamp < this.lastStamp) {
long backMills = lastStamp - currentStamp;
if (backMills <= 5) {
try {
wait(backMills << 1);
currentStamp = System.currentTimeMillis();
if (currentStamp < this.lastStamp) {
}
} catch (InterruptedException e) {
// doNothing
throw new RuntimeException("服务节点时钟发回拔if (currentStamp < this.lastStamp) {
long backMills = lastStamp - currentStamp;
if (backMills <= 5) {
try {
wait(backMills << 1);
currentStamp = System.currentTimeMillis();
if (currentStamp < this.lastStamp) {
throw new RuntimeException("服务节点时钟发回拔,请重点关注!");
}
} catch (InterruptedException e) {
// doNothing
throw new RuntimeException("服务节点时钟发回拔系统异常,请重点关注!");
}
}
},请重点关注!");
}
}
}
long sequence = 0;
if (currentStamp == this.lastStamp) {
sequence = (sequence + 1) & this.maxSequenceValue;// 位运算保证始终就是在4096这个范围内
// 1毫秒内自增数超过设置的最大值,等待1ms,防止进位导致id重复
if (sequence > this.maxSequenceValue) {
sequence = 0L;// 此处不采用随机数是考虑到现有的都不够用了,就别在随机了,搞不好随机个值就更少了
currentStamp = getNextMills();
}
} else {
// 时间不相同时,默认sequence归0,容易导致大部分尾数雷同,采用随机数可以做到数据存储的分布均衡
sequence = RANDOM.nextInt(100);
}
// 维护最后一次生成的时间
this.lastStamp = currentStamp;
// 先按照雪花算法的结构对各part的值进行位移,然后做各个part的或运算就可以拿到最终的结果
long ts = (currentStamp - TIMESTAMP) << this.timestampLeftShift;
long dc = this.datacenterId << this.datacenterIdShift;
long work = this.machineId << this.machineIdShift;
return ts | dc | work | sequence;
}
/**
* 判断是否在1毫秒内
* - 是:阻塞,直到下一毫秒,返回下一毫秒的时间戳
* - 否:返回当前时间戳
*/
private long getNextMills() {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastStamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 静态工厂
*
* @param machineId 机器/节点id[0,1024]
* @param machineBitNum 机器码占用位数
* @param sequenceBitNum 随机数长度
* @return 生成器
*/
public static DistributeIdGenerator getInstance(Integer machineId, Integer machineBitNum, Integer sequenceBitNum) {
if (machineId < 0 || machineId > (~(-1 << machineBitNum))) {
throw new RuntimeException("机器id越界,请重新设置");
}
DistributeIdGenerator generator = new DistributeIdGenerator();
generator.setMachineBitNum(machineBitNum);
generator.setSequenceBitNum(sequenceBitNum);
generator.setMachineId(machineId);
// 初始化偏移量
generator.initShiftOffset();
return generator;
}
public static void main(String args[]) {
try {
DistributeIdGenerator instance = DistributeIdGenerator.getInstance(1, 10, 12);
System.out.println("id:" + instance.nextId());
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在考虑限流时,需要对服务的上下链路,以及流量从前到后涉及的中间件有大概的链路;
举一个我们公司的例子:
http请求->DNS服务器->四层负载->七层负载->入口网关->k8s服务集群->节点前置nginx->机器节点->API接口
这是我们公司一个完整的http请求的链路经过的完整链路;
限流的对象,以及限流的目的要知悉,在整个链路从前到后的过程中,很多的中间件都有机制对流量做控制,关键还是看是技术性限制 OR 业务性限制,下面主要说的是业务要求的限制方案,比如做活动的时候,需要考虑到限流的问题;
在考虑限流之前,需要和PM进行流量的预估,虽然不能准确的预估出大概,但是相对于开发,PM肯定要对设计的场景有一个大概的判断,这一点很关键,需要和PM对齐;
熔断、降级、限流都是一种策略,cloud和dubbo都有机制,包括第三方组件sentential的引入去做,这块只是方案上要去做完善,面试的时候不需要回答的太细,更侧重于全局的角度考虑问题,尤其要结合现有公司的基础架构和微服务生态;
有的公司有自己的业务路由配置网关,也可以做限流,但其实和服务调用层面是一个道理,只不过一个是集中控制,一个是分散到单集群控制;
提一嘴,限流和防重复提交分布式锁要分清楚
目前成熟的限流算法策略大概有下面几种,渗透到一些中间件的配置化限流机制和sdk中,了解清楚会更让我们选择合适的算法去适配场景;
计数器算法是最简单的限流算法之一,在一段时间间隔内(时间窗/时间区间),处理请求的最大数量固定,超过部分不做处理。
简单粗暴,比如指定线程池大小,指定数据库连接池大小、nginx连接数等,这都属于计数器算法。
public class CounterLimiter {
private long timeStamp = System.currentTimeMillis();
private int reqCount = 0;
// 时间窗口内最大请求数
private final int limit = 100;
// 时间窗口ms
private final long interval = 1000;
public boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - timeStamp < interval) {
reqCount++;
return reqCount <= limit;
} else {
timeStamp = now;
reqCount = 1;
return true;
}
}
}
缺点:涉及到窗口的临界问题,容易被攻击者利用漏洞,直接打满接口请求次,导致后面的时间段都无法恢复正常访问
计数器算法的临界问题,可以使用滑动窗口算法来进行增强;
相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多。
规则如下,假设时间窗口为 1 秒:
记录每次请求的时间
统计每次请求的时间 至 往前推1秒这个时间窗口内请求数,并且 1 秒前的数据可以删除。
统计的请求数小于阈值就记录这个请求的时间,并允许通过,反之拒绝。
相当于用一个map或者数组,进行秒级别的访问次数存储;
但是滑动窗口和固定窗口都无法解决短时间之内集中流量的突击。
import java.util.concurrent.atomic.AtomicInteger;
public class SlidingWindowLimiter {
private AtomicInteger[] timeSlots;
private int timeSlotSize;
private int slotIndex = 0;
private long lastTime;
private final int limit;
public SlidingWindowLimiter(int timeSlotSize, int limit) {
this.timeSlots = new AtomicInteger[timeSlotSize];
this.timeSlotSize = timeSlotSize;
this.lastTime = System.currentTimeMillis();
this.limit = limit;
for (int i = 0; i < timeSlotSize; ++i) {
timeSlots[i] = new AtomicInteger(0);
}
}
public boolean tryAcquire() {
long now = System.currentTimeMillis();
synchronized (this) {
if (now - lastTime > timeSlotSize) {
timeSlots[slotIndex].set(0);
slotIndex = (slotIndex + 1) % timeSlotSize;
lastTime = now;
}
int count = 0;
for (AtomicInteger slot : timeSlots) {
count += slot.get();
}
if (count < limit) {
timeSlots[slotIndex].getAndIncrement();
return true;
} else {
return false;
}
}
}
}
可以解决计数器时间窗口和流量突发的问题;漏桶算法将请求视作流入到漏桶里的水,水漏出的速率是固定的,如果漏桶满了,新流入的水将被溢出(即请求被丢弃)。
public class LeakyBucketLimiter {
// 桶的容量
private long capacity = 10;
// 漏桶流出的速率
private long rate = 1;
// 当前水量(当前累积请求数)
private long water = 0;
// 上一次漏水时间
private long lastTime = System.currentTimeMillis();
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
water = Math.max(0, water - (now - lastTime) * rate);
lastTime = now;
if ((water + 1) < capacity) {
water++;
return true;
} else {
return false;
}
}
}
主要关注的点是桶的流速是否超过容量
guava的RateLimiter就是其中的一个应用
该算法通过以恒定的速度向桶中添加令牌,并且每当有请求来时,需要从桶中取出一个或多个令牌才能继续执行。如果桶中没有足够的令牌,请求将被限流,即延迟处理或拒绝服务。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class TokenBucketLimiter {
private final long maxBucketSize;
private final long refillRate;
private AtomicLong currentBucketSize;
private AtomicLong lastRefillTimestamp;
public TokenBucketLimiter(long maxBucketSize, long refillRate) {
this.maxBucketSize = maxBucketSize;
this.refillRate = refillRate;
this.currentBucketSize = new AtomicLong(0);
this.lastRefillTimestamp = new AtomicLong(System.nanoTime());
}
public boolean tryAcquire() {
refill();
if (currentBucketSize.get() > 0) {
currentBucketSize.decrementAndGet();
return true;
} else {
return false;
}
}
private void refill() {
long now = System.nanoTime();
long refillTokens = (now - lastRefillTimestamp.get()) / 1_000_000_000 * refillRate;
if (refillTokens > 0) {
long adjustedRefill = Math.min(refillTokens, maxBucketSize - currentBucketSize.get());
currentBucketSize.addAndGet(adjustedRefill);
lastRefillTimestamp.set(now);
}
}
}
和漏桶算法的区别就在于一个是加法,一个是减法。
可看出令牌桶在应对突发流量的时候,桶内假如有 100 个令牌,那么这 100 个令牌可以马上被取走,而不像漏桶那样匀速的消费。所以在应对突发流量的时候令牌桶表现更佳。
@Service
public class AccessLimitService {
//每秒只发出5个令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
/**
* 尝试获取令牌
* @return
*/
public boolean tryAcquire(){
return rateLimiter.tryAcquire();
}
}
@Controller
public class HelloController {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Autowired
private AccessLimitService accessLimitService;
@RequestMapping("/access")
@ResponseBody
public String access(){
//尝试获取令牌
if(accessLimitService.tryAcquire()){
//模拟业务执行500毫秒
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
return "aceess success [" + sdf.format(new Date()) + "]";
}else{
return "aceess limit [" + sdf.format(new Date()) + "]";
}
}
}
短网址现在可以说是随处可见,很多短信内部都会包含短网址,点击短网址链接可以直接跳到对应的长链接地址,背后的原理其实很简单,服务端会存储短链和长链的对应关系,当接受到短链请求后,程序会去程序中查找对应的长链然后给浏览器返回给浏览器并且让浏览器重定向到长链地址,这就是整个流程;
短网址的长度该设计为多少呢? 当前互联网上的网页总数大概是 45亿(参考 http://www.worldwidewebsize.com),超过了 232=4294967296,那么用一个64位整数足够了。
一个64位整数如何转化为字符串呢?,假设我们只是用大小写字母加数字,那么可以看做是62进制数,log62(264−1)=10.7,即字符串最长11就足够了。
实际生产中,还可以再短一点,比如新浪微博采用的长度就是7,因为 627=3521614606208,这个量级远远超过互联网上的URL总数了,绝对够用了。
现代的web服务器(例如Apache, Nginx)大部分都区分URL里的大小写了,所以用大小写字母来区分不同的URL是没问题的。
因此,正确答案:长度不超过7的字符串,由大小写字母加数字共62个字母组成
短链算法:
最容易想到的办法是哈希,先hash得到一个64位整数,将它转化为62进制整,截取低7位即可。但是哈希算法会有冲突,如何处理冲突呢,又是一个麻烦。这个方法只是转移了矛盾,没有解决矛盾,抛弃。【拥抱雪花算法】
短链转码:Base62 算法是一种将数据转换为由 62 个字符组成的字符串的编码方式,这 62 个字符通常包括 26 个大写英文字母、26 个小写英文字母和 10 个数字,以下是其相关介绍:
Base62 算法的基本原理是将数据按照 62 进制进行编码。它把要编码的数据看作是一个大的整数,然后将这个整数转换为 62 进制的表示形式,每一位对应 62 个字符中的一个。
以对数字进行 Base62 编码为例,首先确定数字对应的十进制值,然后用这个十进制数除以 62,得到商和余数。余数就是对应 62 进制下的一位数字,其范围是 0 到 61,将余数映射到对应的字符。接着,用商继续进行上述除法操作,直到商为 0。最后,将得到的所有字符按照从后往前的顺序排列,就得到了 Base62 编码后的字符串。
与编码过程相反,解码时是将 Base62 编码的字符串中的每个字符映射回对应的数字,然后将这些数字按照 62 进制转换为十进制数,从而还原出原始数据。
答案:雪花算法+ Base62算法
存储到数据库,这样短链映射的长链数据参数,方便后续对数据统计和分析
布隆过滤器或者redis缓存
IP单日/秒限流
@Autowired
private UrlMapMapper urlMapMapper;
/**
* 根据短链查询实际的长链 & 重定向
*/
@GetMapping("/get")
public void longUrl2Short(@RequestParam String shortUrl, HttpServletResponse response) throws IOException {
// DB中查询
QueryWrapper<UrlMap> wrapper = new QueryWrapper<>();
wrapper.eq("short_url", shortUrl);
UrlMap urlMap = urlMapMapper.selectOne(wrapper);
if (urlMap == null) {
throw new IllegalArgumentException("短链不存在");
}
log.info("短链存在且重定向:{}", JSON.toJSONString(urlMap));
// 重定向
sendRedirect(urlMap.getLongUrl(), response);
}
/**
* 根据长链生成短链
*/
@GetMapping("/shorten")
public ResponseEntity<String> createShortUrlV3(@RequestParam String longUrl) {
// 1、先查询缓存里面是否对应的短链。缓存中有这个短链,直接返回
// 2、再查询数据库里面是否有长链 对应的短链
QueryWrapper<UrlMap> wrapper = new QueryWrapper<>();
wrapper.eq("long_url", longUrl);
UrlMap urlMap = urlMapMapper.selectOne(wrapper);
String shortUrl = urlMap != null ? urlMap.getShortUrl() : null;
if (shortUrl != null && !shortUrl.isEmpty()) {
// 数据库中有这个短链,直接返回,可以顺便保存到缓存中
return ResponseEntity.ok(shortUrl);
}
// 3、没找到, 那就利用雪花算法生成ID
Long id = DistributeIdGenerator.getInstance(1, 10, 12).nextId();
// 利用base62算法,生成短链
shortUrl = Base62.convertToShortUrl(id);
// 4.存储到数据库
// 4.1、将短链保存到布隆过滤器中
// 4.2、保存到缓存中, 以便下次查询
// 4.3、保存到数据库
urlMapMapper.insert(new UrlMap(longUrl, shortUrl));
return ResponseEntity.ok(shortUrl);
}
// 进行重定向的函数
public void sendRedirect(String longUrl, HttpServletResponse response) throws IOException {
response.sendRedirect(longUrl);
response.setHeader("Location", longUrl);
response.setHeader("Connection", "close");
response.setHeader("Content-Type", "text/html; charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
}
package com.example.sharding.config;
public class Base62 {
/**
* ASCII编码
* Value Encoding Value Encoding Value Encoding Value Encoding
* 0 a 17 r 34 I 51 Z
* 1 b 18 s 35 J 52 0
* 2 c 19 t 36 K 53 1
* 3 d 20 u 37 L 54 2
* 4 e 21 v 38 M 55 3
* 5 f 22 w 39 N 56 4
* 6 g 23 x 40 O 57 5
* 7 h 24 y 41 P 58 6
* 8 i 25 z 42 Q 59 7
* 9 j 26 A 43 R 60 8
* 10 k 27 B 44 S 61 9
* 11 l 28 C 45 T
* 12 m 29 D 46 U
* 13 n 30 E 47 V
* 14 o 31 F 48 W
* 15 p 32 G 49 X
* 16 q 33 H 50 Y
*/
// 定义 62 进制字符集
private static final String CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = CHARSET.length();
private static final int SHORT_URL_LENGTH = 7;// 短链长度
/**
* 将雪花算法生成的 ID 转换为 7 位短链【哈希算法有冲突,加盐或者其他加密只是减缓了冲突机率】
*
* @param snowflakeId 雪花算法生成的 ID
* @return 7 位短链
*/
public static String convertToShortUrl(long snowflakeId) {
StringBuilder shortUrl = new StringBuilder();
// 进行进制转换
while (snowflakeId > 0) {
int remainder = (int) (snowflakeId % BASE);
shortUrl.insert(0, CHARSET.charAt(remainder));
snowflakeId /= BASE;
}
// 不足 7 位时在前面补 0
while (shortUrl.length() < SHORT_URL_LENGTH) {
shortUrl.insert(0, CHARSET.charAt(0));
}
return shortUrl.toString();
}
public static void main(String[] args) {
// 示例雪花算法生成的 ID
long snowflakeId = 1234567890123L;
String shortUrl = convertToShortUrl(snowflakeId);
System.out.println("雪花算法 ID: " + snowflakeId);
System.out.println("生成的 " + SHORT_URL_LENGTH + " 位短链: " + shortUrl);
}
}