这篇文章只是单纯的对秒杀场景下商品详情页访问如何抗住高并发和提高吞吐量的一些设计想法,不对秒杀业务逻辑进行过多的分析,以及不进行实战代码解析,只是理论分析。
一、普通商品详情处理逻辑
用户点击商品详情,后台根据商品ID查询数据库获得商品详情(商品价格、介绍、评论、图片、套餐等),伪代码如下:
//1、根据商品ID查询商品详情
MerchDetail detail = MySQL.getMerchDetail(id);
//2、返回商品详情
return detail
这段逻辑代码在普通商品详情来说是没有问题的,虽然第一步去查询数据库需要连表查询很多详情有关的信息,会有一定的耗时,但是访问量不大的情况下。正常服务器吞吐量应该是OK的,但是如果是秒杀商品详情的话,应该是不满足要求的,压力直接打到DB,会一瞬间打死DB,那么我们就需要想办法降低数据库的压力。
二、借助REDIS做缓存来降低数据库的压力
我们可以将代码改为如下:
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//4、设置redis
if(detail!=null){
REDIS.set(key,detail);
}
}
//5、返回商品详情
return detail
先根据ID从redis获取商品详情,如果不存在再从数据库中获取放入redis中,然后返回,正常来说的话加上redis后吞吐量将会比不加redis高个10倍左右,后续有时间会基于这篇文档进行实验验证。
但是上面的代码其实是有点问题的,因为我们的商品详情肯定是会设置有效期的,并且就算再秒杀期间都有效,详情再第一次访问进来的时候redis中是没有的,那么高并发情况下可能会导致几百上千个线程同时到达第三步,那么还是可能会压垮我们的数据库。那怎么办呢?
1、用分布式锁,值允许单个线程去数据库中查数据
如果担心上面说的那种情况,我们可以使用分布式锁来让一个线程来去DB查询商品详情,分布式锁客以借助zookeeper或者redis来实现,各有各的优缺点,当然我们也可以进行数据预热,但是进行预热也可能因为其他原因导致redis中的数据失效了,如果可以确保redis中的数据不会因为其他原因失效,则可以采取预先预热的方式,下面是redis分布式锁来实现的伪代码
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
//4、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
if(detail!=null){
//5、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//6、设置redis
if(detail!=null){
REDIS.set(key,detail);
}
//7、释放锁
Redis.unLock(id);
}
}
}
//8、返回商品详情
return detail
第4步如果线程获得锁后,应该再查询下是否有值,因为第一个线程以及放进去了,当然我们可以释放锁后Redis.lock(id)就直接返回false,那么代码就可以改为下面这种:
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
if(detail!=null){
//4、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//5、设置redis
if(detail!=null){
REDIS.set(key,detail);
}
//6、释放锁
Redis.unLock(id);
}
}else{
//7、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
}
}
//8、返回商品详情
return detail
虽然这里是可以保证只有单个线程压到DB了,但是如果有人用不存在的id来刷怎么办,这种情况下从redis肯定是获取不到数据的,最后全部都会打到DB中,其实如果我们借助了上面的分布式锁的话,如果同一个我们会有分布式锁来保证只有一个线程打到DB,那么我们只需要从DB中查不到后页放一个值再REDIS中告知没有该ID即可,代码如下:
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
if(detail!=null){
//4、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//5、设置redis
if(detail!=null){
REDIS.set(key,detail);
}else{
REDIS.set(key,"商品不存在");
}
//6、释放锁
Redis.unLock(id);
}
}else{
//7、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
}
}
//8、返回商品详情
return detail
这样子就可以了,但是如果有人一致变换不重复的ID来刷,这样子的话美俄个ID都会去查DB,并且每个结果都得放入REDIS,很明显这个是不可取的,那怎么办呢?
2、提前将要秒杀的商品ID放入REDIS中
这样子我们再从REDIS查询的时候先检查该ID是否存在,若不存在就直接返回不存在即可,伪代码如下:
//先检查该ID是否存在
if(REDIS.checkId(id)){
return "商品不存在";
}
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
if(detail!=null){
//4、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//5、设置redis
if(detail!=null){
REDIS.set(key,detail);
}else{
REDIS.set(key,"商品不存在");
}
//6、释放锁
Redis.unLock(id);
}
}else{
//7、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
}
}
//8、返回商品详情
return detail
嗯!到这里也许你会认为应该完美了,如果对于秒杀商品量少的话,可以采取这种方法,如果秒杀商品很多I呢,比如成百上千万或者说亿,那些大公司比如淘宝京东拼多多是完全可能的,再按商品ID是雪花算法算是20位,这里假设用UTF-8来编码,数字是1个字节,20位的数字就20字节,一百万就对应2千万字节也就是20M的key,一亿就对应2G,如果是10亿是200G内存,只是用于保存商品ID就耗费这么多珍贵的内存,所以都用用redis保存再内存中肯定是不科学的,那怎么办呢?
这里可以采用布隆过滤器,先把所有的商品ID加入布隆过滤器中,然后逻辑就变成了如下:
//先检查该ID是否存在
if(BLGLQ.checkId(id)){
return "商品不存在";
}
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
if(detail!=null){
//4、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//5、设置redis
if(detail!=null){
REDIS.set(key,detail);
}else{
REDIS.set(key,"商品不存在");
}
//6、释放锁
Redis.unLock(id);
}
}else{
//7、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
}
}
//8、返回商品详情
return detail
当然布隆过滤器最好是再活动开始之前就热更新加载好,这样就可以解决上面的问题了,布隆过滤器检查失败的就用后面的分布式锁来解决,这样就趋于完美了,具体布隆过滤器的原理这里就不进一步说明了,自行网上搜搜,这里只是简要说下:布隆过滤器就是通过多个hash函数将ID映射到一个数组的不同的位置,将那些位置改为1,所以如果某一个ID映射的值有部位1的,则表明该ID一定不存在,但是如果全是1也不一定表面ID就一定存在,毕竟是hash所以会存在hash冲突的问题,不过布隆过滤器可以设置准确率。
那上面的方法就完美了么?一般来说是到这个层度已经很棒了,但是还是可以继续优化的,如下。
三、借助本地JVM缓存来优化性能
REDIS操作无论如何都设计了远程TCP的调用,而我们的网卡以及网络传输连接建和释放都会影响性能的,那么这里可以用JVM来做一级缓存,把redis当作二级缓存,而为了保证缓存可以失效,这里可以引入比较成熟的缓存框架比如这里推荐Guava的Cache,该框架特备方便的做缓存,还支持各种失效的移除策略,大家可以自行去网上搜索,当然我们也可以自己借助ConcurrenthashMap来实现,但是为啥要重复造轮子呢?
//先检查该ID是否存在
if(BLGLQ.checkId(id)){
return "商品不存在";
}
MerchDetail detail = JVM.getMerchDetail(key);
if(detail==null){
//1、从redis获取商品详情
MerchDetail detail = REDIS.getMerchDetail(key);
//2、如果redis为空,则从数据库中获取然后初始化到redis中
if(detail==null){
//3、其它线程将阻塞在这里
if(Redis.lock(id)){
if(detail!=null){
//4、根据商品ID查询商品详情
detail = MySQL.getMerchDetail(id);
//5、设置redis
if(detail!=null){
REDIS.set(key,detail);
}else{
REDIS.set(key,"商品不存在");
}
//6、释放锁
Redis.unLock(id);
}
}else{
//7、若获取锁,则再次从redis获取值
detail = REDIS.getMerchDetail(key);
}
}
if(detail!=null){
JVM.set(key,detail);
}
}
//8、返回商品详情
return detail
正常来说,上面应该已经基本上满足日常业务需求了,但是如果是那种上百万上亿的访问并发呢?如果请求全部都打到我们的服务器。服务器可能抗不住,怎么办呢?
四、详情页面静态化,借助CDN来做缓存
此时我们就需要将详情页面静态化,毕竟秒杀详情一般再秒杀期间都不允许改变的,所以可以将其变为静态化资源,然后借助CDN来做缓存,这样压力就都在各大CDN厂商了,完美,只不过需要多费点钱买CDN而已!
也许又有人会问,如果编程静态后,那页面的按钮以及评论怎么刷新呢?
这里要强调下,正常来说秒杀的商品详情是基本上不会怎么变化的,而且跟普通商品详情还有点区别,一般没有什么优惠券那些,评论什么的页基本上不会变化。而页面的按钮呢,我们可以用一个js从我们的服务器来读取时间来控制,只需要再哪个独立的js后面加个随机变化的随机数就不会放到CDN中了,而这个js通常我们是放在nginx服务器上就可以了,单单一个小小的js文件,我想我们的服务器还是可以扛得住的!
也许又有人会问,那服务器的js文件怎么更新呢,那这个我们看了一把该js文件放到分布式文件存储上面,如果秒杀时间开始了,程序就会新生成一个js文件放到文件服务器,然后nginx那里就可以直接去文件服务器获取。
好了,暂时先打住,我上面只是理论分析可能会遇到的各种情况,具体深究下去肯定还有很多需要考虑的地方,或者也有更好的解决方案,但是逢山开路,遇水搭桥,有问题就解决呗!供参考!