SaaS短链接系统
短链接生成算法
简历内容:通过布隆过滤器完成判断短链接是否已存在,性能远胜分布式锁搭配查询数据库方案
存在哈希冲突怎么办?
如果冲突了,就去重新生成,然后设置一个重设次数,不停的重新生成短链接,如果一直失败,抛出异常。(然后这里判断短链接是否重复生成也是通过布隆过滤器来进行判断的,这样远远比通过分布式锁查询数据库的效率高) 布隆过滤器可能存在一定误判,如果出现误判了,尝试插入数据库,如果出现重复,则抛出异常
但是这样子可能还是有问题,我们可以对每一个链接,给这个链接生成一个分布式ID,然后用这个分布式ID拼接上原始链接作为输入,因为同一时刻可能会有多个相同的url来进行尝试,这样就会造成冲突,使用murmurHash进行生成128bit的哈希值然后转换为62进制的数,添加一个分布式ID是为了减少哈希冲突,但是这样并不能完全避免保证哈希冲突,(加一个分布式ID主要是为了防止某一时刻有多个线程对同一个链接要求生成短链接,那么这样有很大概率产生哈希冲突,为了避免哈希冲突,需要加一个标记,我这里使用的是雪花算法生成的分布式ID,用这个和短链接拼接来进行生成)
什么是分布式ID
然后判断一个短链接是否已经生成了使用布隆过滤器
布隆过滤器
BKDRHash布隆过滤器中使用这种哈希函数,把字符串映射成数字,然后表示各个位置
为了兼容短链接后管用户分页查看短链接功能,在短链接数据分片的基础上增加路由表完成跳转功能。
在这里采用了分库分表的思想,一个相关博客是分库分表描述
所以有个问题,按GID分的最后结果是怎样的?插入是插入到哪个数据库
封装缓存不存在读取功能,通过双重判定锁优化更新或失效场景下大量查询数据库问题
视频位于,短链接跳转原始链接功能(缓存击穿)
if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
lock.lock();
try {
originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
if (StrUtil.isNotBlank(originalLink)) { //这里就是双重判定锁,防止一个多线程来取得时候,第一个线程发现缓存没有给添加到数据库中然后给添加到缓存中,后面来的请求依然访问数据库,给数据库带来极大的压力
ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
shortLinkStats(fullShortUrl, null, statsRecord);
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if (shortLinkGotoDO == null) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
((HttpServletResponse) response).sendRedirect("/page/notfound");
return;
}
stringRedisTemplate.opsForValue().set(
String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
shortLinkDO.getOriginUrl(),
LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
);
ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
} finally {
lock.unlock();
}
这一部分的功能主要是,首先从redis根据短链接key来看有没有缓存,如果有就直接查出原始链接的缓存了,如果没有这时我们需要去数据库来查找
而此时需要首先加一个锁,再来查找数据库,查完数据库时要把查询出来的数据放到redis中同时设置过期时间,如果不使用DCL双重判定锁,那么可能一个线程执行完成后,释放锁了,另一个线程卡在了获取锁的过程了,此时缓存中已经有了数据,但是这个线程还是获取到了锁,对数据库进行查询了,因此需要再获取锁后,添加一个判断缓存中是否已经有数据的判断,实现双重判定锁,来减少对数据库的查询和开销。
通过异步更新缓存,保障短链接缓存与数据库之间的数据一致性功能
对应八股,如何保持数据库和缓存之间的一致性?
讲解视频
- 延时双删(针对于先删缓存再删数据库的情况),更改数据库后间隔一定时间后删除redis中的内容,这样就可以防止有的线程发现缓存中没有数据后,又把未更改的数据写会缓存中去
- 使用消息队列,修改数据库后,将要修改的内容放入消息队列中,消费者取出后再进行修改,从而实现异步更新。这样能够达到最终一致性但是无法满足强一致性
- 还有一种是订阅mysql的binlog缓存来进行更新
通过 Redis 完成消息队列消费业务下的幂等场景,保障消息在一定时间内消费且仅消费一次
什么是幂等
redis作为消息队列使用
redis解决幂等问题
相关视频位于:消息队列重构短链接功能
幂等指的是方法被重复多次调用的情况,我们要保证产生的影响和第一次调用产生的影响是相同的,这种问题一般发生于用户重复提交和网络通信中数据丢失出发超时重传
那么解决幂等问题有几种方法:
- 第一种使用唯一id,比如向数据库中插入商品订单,那么订单id好是主键唯一,那么不会重复
- 第二种是利用redis和消息队列,为了避免消息队列中的消息重复消费,在redis中对每个消息队列的消息id使用setnx设置到redis中,这样在消费时可以进行判断,是否消息已经消费国
- 第三种是使用状态机
sentinel
RocketMQ
Kafka
抽奖系统
八股记录
mysql构建分布式数据库
http状态码
https加密过程
https通过SLS进行加密,其中用到了对称加密和非对称加密的形式
- 客户端生成一个随机数发送到服务器
- 服务器端有自己的公钥和私钥对,然后服务器会把自己的公钥发送到客户端上,服务器还会生生成一个随机数,发送到客户端
- 客户端会生成第三个随机数,也叫做预主秘钥,然后客户端会把这个预主秘钥用公钥加密后发送到服务器上
- 服务器收到加密后的预主密钥后,用自己的私钥解密,
- 然后客户端和服务端都用第一随机数,第二随机数和预主密钥计算出会话秘钥,然后双方使用这个会话秘钥,进行信息加密和解密,来进行对称加密的交流(后来因为会频繁交互,使用非对称加密的形式会导致资源消耗比较高,所以采用了这种方法)
TCP释放链接时等待2MSL
java二维数组排序
Arrays.sort(intervals,new Comparator<int[]>(){
@Override
public int compare(int[] a,int[] b){
if(a[0] > b[0]) return 1;
return -1;
}
});
Python实现给定两个[1,7]中的随机数,生成一个[1,9]的等概率随机数
import random
def generate_random_1_to_9():
# 生成两个[1,7]之间的随机数
rand1 = random.randint(1, 7)
rand2 = random.randint(1, 7)
# 将两个[1,7]的随机数映射到[1,9]
mapped_result = (rand1 + rand2) % 9 + 1
return mapped_result
# 测试
for _ in range(10):
print(generate_random_1_to_9())
docker相关
查看线程信息ps -elf
docker文件挂载命令 docker run -v /host/path:/container/path …
docker端口映射命令 docker run -p host_port:container_port …
docker端口映射的原理
Docker 端口映射的原理涉及到 Docker 的网络模型以及 Linux 内核中的网络命名空间和端口转发机制。
Docker 网络模型:
Docker 默认使用的网络模型是“桥接(bridge)”模式。在这种模式下,Docker 创建一个虚拟的网络桥接口(通常是 docker0),容器连接到这个桥接口上,并且可以相互通信。在端口映射中,Docker 会利用 Linux 的网络命名空间和 iptables 规则来实现将主机上的端口映射到容器内部的端口。
网络命名空间:
Docker 会为每个容器创建一个独立的网络命名空间。网络命名空间是 Linux 内核的一个特性,它将网络设备、IP 地址、路由表等网络资源隔离开来,使得每个命名空间中的网络配置都是独立的。这样,容器之间和容器与宿主机之间的网络不会互相干扰。
端口转发:
Docker 使用 Linux 内核的端口转发机制来实现端口映射。具体来说,Docker 会在宿主机上创建一个 iptables 规则,将主机上指定的端口(如 8888)转发到容器内部的对应端口(如 8080)。这样,当主机收到对 8888 端口的请求时,Linux 内核会将请求转发给容器内的 8080 端口,从而实现了端口映射。
总体来说,Docker 端口映射的原理是利用 Linux 内核的网络命名空间和端口转发机制,在宿主机和容器之间建立起端口映射关系,使得容器内的服务能够通过主机上的端口对外提供访问。
python相关
基本数据结构。 列表(lists) 元组(tuple) 集合(set) 字典(dic) 字符串(Stirng)
python偏函数
IO多路复用
进程线程的区别
执行单位:
进程是程序的执行实例,拥有自己的地址空间、内存、文件描述符等资源。每个进程都是独立的,彼此之间不能直接共享数据,进程之间的通信需要借助于操作系统提供的通信机制(如管道、消息队列等)。
线程是进程内部的执行单元,共享进程的地址空间和资源。一个进程中的多个线程可以访问相同的内存和文件描述符,因此线程之间的通信更加简便,可以直接读写共享的数据。
创建和销毁开销:
进程的创建和销毁通常比较耗费资源,因为每个进程都需要分配独立的地址空间、文件描述符等资源。
线程的创建和销毁比进程轻量级,因为它们共享进程的资源,只需要创建线程私有的执行栈等少量资源。
并发性和并行性:
进程之间的并发性体现在它们在同一时刻可以处于运行、就绪、阻塞等状态,操作系统会根据调度算法来进行进程间的切换,以实现多个进程的并发执行。
线程之间的并发性体现在它们可以同时执行,由于线程共享进程的地址空间,因此线程之间的切换更加快速,可以实现更高效的并发。
调度和同步:
进程之间的调度和同步需要操作系统来进行管理,通常使用进程间通信(IPC)机制来实现进程间的同步和数据传输。
线程之间的调度和同步通常是通过线程同步原语(如锁、信号量、条件变量等)来实现的,因为它们共享相同的地址空间,可以直接对共享数据进行访问和操作。
僵尸进程和孤儿进程和守护进程
Mysql索引失效和Mysql事务隔离级别
- 使用左或者左右模糊查询
- 使用where or子句时,如果前一个是索引后一个不是索引会导致索引失效
- 索引进行表达式求值会造成索引失效
- 索引进行隐式类型转换时会导致索引失效
- 索引进行函数计算时会导致索引失效
- 联合索引不满足最左匹配时会导致索引失效
B树和B+树相关
DNS相关
DNS服务器分为根域名服务器,顶级域名服务器,权威域名服务器
然后查询有递归查询和迭代查询两种方法。
输入url的流程
初始化(当一个机器加入到网络中时)首先通过DHCP(使用UDP)请求报文,向DHCP服务器请求一个IP地址,而DHCP服务器会返回分配的IP地址和DNS服务器的IP地址
下面开始输入一个url的转化过程:
- 首先客户端服务器会生成一个DNS查询报文(DNS是使用UDP的,且端口号为53),而此时这个查询报文需要传递到对应的DNS服务器上,但是这个时候可能还不知道DNS服务器的MAC地址,这时就需要通过ARP协议(发送一个广播请求来获得MAC地址),获得DNS服务器的MAC地址,这样就可将报文发送给对应的DNS服务器了。(这里如果DNS服务器和客户机器不在一个网段中,还需要通过BGP协议等来寻找)
- 当客户端获得了url对应的ip地址后,生成TCP套接字,执行三次握手建立TCP链接,这个生成的套接字用来向对应IP地址发送http报文,之后相应的信息通过BGP协议等,在不同的网段中路由转发,最后到达服务器,服务器抽取出http报文,来生成http响应报文,来返回相应数据,最后这些数据被封装后经过路由转发回到了客户端,至此完成了交互的过程。
如果我输入某个域名,想让他不访问这个ip要怎么办
要阻止计算机访问特定域名对应的IP地址,你可以通过修改操作系统的 hosts 文件来实现。Hosts 文件位于操作系统的系统目录下,用于将域名映射到特定的IP地址。通过编辑这个文件,你可以将指定的域名映射到一个无效的IP地址,从而阻止计算机访问该域名。
排序算法
设计模式
设计模式
饿汉单例是线程安全的
懒汉单例是非线程安全的,需要通过加锁写法
红黑树
定义
定义:
- 红黑树的所有节点的颜色要么是红色的,要么是黑色的
- 红黑树中不能有连续两个红色节点
- 红黑树中从任意一个节点出发,到空叶节点经过的黑色节点数相同
- 红黑树的所有空叶节点(也就是外部节点)都是黑色的
- 红黑树的根节点是黑色的
插入
红黑树的插入:
按照BST的方法,找到对应位置插入,然后将这个节点涂成红色
7. 若这个节点是根节点,那么将这个节点颜色染黑即可
8. 若这个节点的父节点为黑色节点,不需要任何操作
9. 若这个节点的父节点为红色节点,叔叔节点为红色节点,那么将叔叔节点和父节点染成黑色,将爷爷节点染成红色,此时将爷爷节点看作是新插入的节点,递归处理
10. 父节点是红色,叔叔节点是黑色时:
(1) (父节点是左孩子,插入节点也是左孩子)或者(父节点是右孩子,插入节点也是右孩子): 将父节点和爷爷节点颜色互换,然后对爷爷节点进行一次左旋
(2) (父节点是右孩子,插入节点是左孩子)或者(父节点是左孩子,插入节点是右孩子):对父节点进行左旋,然后将父节点看做新插入的节点,递归处理
删除
注意外部节点(空叶节点)不算作是儿子节点
- 当删除节点有两个儿子时,不能直接对这个节点进行删除,需要先用这个点的直接前驱或者直接后继来填补这个点,然后转化为对直接前驱或直接后继的删除
- 若这个节点只有左子树或者只有右子树: 直接删除,同时子树替代自己的位置,并染黑色
- 当这个节点没有子树时:
(1).节点是红色时,直接删除
(2).节点是黑色,兄弟是红色时: 交换节点和父节点的颜色,同时对父节点做一次左旋,然后删除
复杂度分析
进程间通信的方式
Shell脚本相关
git常用命令
git reset 版本号
回退到对应版本号后再通过add commit等来合并版本号
git commit --amend
只能合并两个版本号
git rebase
死锁
死锁的必要条件
- 请求和保持
- 非剥夺
- 循环等待
- 互斥占有