本篇文章的总结主要来自于本人的主观看法,欢迎各位在评论区指导。
秒杀其实不仅是大家简历上的一些热门,也是场景题中的一些热门考点。
场景描述:秒杀一般指的是针对某个或者某几个特定的产品,有瞬时爆发的QPS轰击过来,频繁的进行数据库的读操作,少量的进行数据库的写操作(扣货扣款)。
设计难点与解决思路:
在支付方面,其实我们一般都用TCC分布式事务去实现,因为这个要求一定的即时性,不能异步去完成主流程(发起支付,检验存货量,余额,扣款扣货)。
另外一点,支付需要考虑幂等性,就是你支付付款的这个流程,不管请求多少次肯定都是只能生效一次的,一般情况下,保证幂等性,我们有以下几个方法:
(1). Token
服务端自己在支付之前产生一个Token扔到Redis里,这个Token和主键进行唯一关联,然后客户端正常请求,我们通过主键快速锁定Token,完成支付,支付成功或者失败,我们都将这个Token在使用过一次之后立刻删除掉。也就保证了一个唯一性。
(2). 数据库去重
我们将订单ID或某个可以保证唯一的字段作为key进行MD5加密,然后和订单ID进行Map,当我们支付的时候,调用一下检验,检验过后,将加密的结果进行删除,也可以保证唯一请求。
这个必须得说,在极限情况下,如果qps特别高,特别集中,我们要隔断数据库,一切放在redis里去做处理,或者如果允许异步的话,也可以隔x秒和数据库做一个同步。
而至于redis用位图去拦截一下网络上的恶意请求这些,就算是基本知识了。
超卖现象,有以下几种方法可以解决:
(1). Redis分布式锁
用分布式锁其实主要是是防止一个用户一秒结算两次,其实意思比较简单,就是加锁,然后有一个释放时间,表示本质上一次只会有一个线程去经过结算的这个步骤。
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
(2). redis的lpush rpop
是redis的一个特性,一次性只进出一个,但是如果要是一次性购买多个东西,就比较慢了,这个方法适用于你只买一个,多线程请求一个物品的情况。
(3). Redis的原子操作+sql乐观锁
首先也是将货品数量扔进redis,在redis完成扣减的计算,但是扣减之前一定要经过一下查询,比如你只有1个,我想要3个,你就得手写一个拒绝的操作。redis是没问题了,然后我们还得保证同步数据库,这个时候,我们可以直接用乐观锁去进行读取扣减了。
这个问题的发生,原理上是无法避免的,但是出现的概率也很低。可以让周围的结点基于nio的方法广播自己的状态,当危险状态的结点接收到了过量的请求,就发出警告,让其他安全结点用RPC转移流量,当然自己也得有一定手段去减免这种情况,或者说,发生了可以用限流熔断降级等等的方法去解决,不过这就得分场景去解决了。
限流其实,我们一般都会用三种方法来完成,分别是计数,漏斗算法或者令牌桶算法,但是世面上更常用的还是令牌桶算法。
以下的几种场景介绍,假如我们整个平台的可容纳QPS上限是100.
计数算法,实际上就是借助AOP,在控制层前放一个拦截器计算请求次数,比如我们QPS是100,那么就把1秒内100次之上的请求放在一个阻塞队列里。
坏处就是,如果在1秒内的前10ms请求了100次,那么后面的990ms只能等着了。
漏斗算法实际上就是对计数算法有了一定的优化,简明扼要的说,漏斗算法,就是让消费变成有规律的,比如,每5ms消费一次请求,漏斗算法的实现,一般都是依靠消息队列去实现的,核心意义就是让消息无规则的产生,然后解耦,让消费者有规律的去消费。
但是坏处就在于,如果要是秒杀类的场景,就会有大量的消息阻塞在队列,这还只是一点,如果要是高并发场景下,消息队列和消费者消费句柄进行连接然后消费的时候,可能会有句柄丢失进行额外阻塞的情况发生。
此外还有一点就是,有消息队列的并发场景,基本写代码都比较复杂,不好维护。
令牌桶算法,实际上就是我们让每一个请求都携带一块令牌然后去消费,当我们没有多余的令牌的时候,就只能阻塞了,另外,令牌是定期产生的,但是永远不会超出规定的阈值。
我们一般使用令牌桶算法,更多的是会采用guava的RateLimiter
public class Test {
public static void main(String[] args) {
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100));
// 指定每秒放1个令牌
RateLimiter limiter = RateLimiter.create(1);
for (int i = 1; i < 50; i++) {
// 请求RateLimiter, 超过permits会被阻塞
//acquire(int permits)函数主要用于获取permits个令牌,并计算需要等待多长时间,进而挂起等待,并将该值返回
Double acquire = null;
//一个RateLimiter主要定义了发放permits的速率。如果没有额外的配置,permits将以固定的速度分配,单位是每秒多少permits。默认情况下,Permits将会被稳定的平缓的发放。
if (i == 1) {
acquire = limiter.acquire(1);
} else if (i == 2) {
acquire = limiter.acquire(10);
} else if (i == 3) {
acquire = limiter.acquire(2);
} else if (i == 4) {
acquire = limiter.acquire(20);
} else {
acquire = limiter.acquire(2);
}
executorService.submit(new Task("获取令牌成功,获取耗:" + acquire + " 第 " + i + " 个任务执行"));
}
}
}
class Task implements Runnable {
String str;
public Task(String str) {
this.str = str;
}
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + str);
}
}
1、情景题:如果一个外卖配送单子要发布,现在有200个骑手都想要接这一单,如何保证只有一个骑手接到单子?
这个可以用redis的lpush rpop来实现。
2、场景题:美团首页每天会从10000个商家里面推荐50个商家置顶,每个商家有一个权值,你如何来推荐?第二天怎么更新推荐的商家?
这个可以用堆排,第二天更新推荐的可以在要更新之前的时候在redis做计算操作,然后放数据库做个同步就行了。
3、场景题:微信抢红包问题
悲观锁,乐观锁,存储过程放在mysql数据库中。
4、场景题:1000个任务,分给10个人做,你怎么分配,先在纸上写个最简单的版本,然后优化。
全局队列,把1000任务放在一个队列里面,然后每个人都是取,完成任务。
分为10个队列,每个人分别到自己对应的队列中去取务。
5、场景题:保证发送消息的有序性,消息处理的有序性。
给消息加一个header,识别一下header的syn就行
6、如何把一个文件快速下发到100w个服务器
边下发,边复制
7、给每个组分配不同的IP段,怎么设计一种结构使的快速得知IP是哪个组的?
8、10亿个数,找出最大的10个。
建议一个大小为10的小根堆。
9、有几台机器存储着几亿淘宝搜索日志,你只有一台2g的电脑,怎么选出搜索热度最高的十个搜索关键词?
分bucket,每个bucket找出频次最高的10个,总体用分治算法做。
10、分布式集群中如何保证线程安全?
分布式锁,意在让分布式多线程的环境针对一个共享资源一次性只会有一个线程获取到锁里的资源。分布式锁的实现一般就是redis和zk居多。redis设置一下expire time就行。
11、10万个数,输出从小到大?
先划分成多个小文件,送进内存排序,然后再采用多路归并排序。
12、有十万个单词,找出重复次数最高十个?
map