最近离职,总结下公司做过的秒杀系统。秒杀系统分为两类:1、预约性秒杀系统(茅台);2、非预约性秒杀(春节火车票抢购或商城的日常秒杀)。
先说下秒杀系统的特点:1、短时间大流程,2、限时、限量、限价。
秒杀系统的常用架构:
秒杀系统常需要考虑的问题,我们从下面几个角度考虑
1、稳定性,无论在什么场景下,都需要保证系统的稳定。
- 隔离:首先针对秒杀的业务进行资源隔离,包括服务器,域名,LBS,Redis进行隔离或者数据库的隔离,保证不会因为秒杀引起其它系统的稳定性问题;
- 监控和链路追踪:监控报警包括服务主机的监控预警、服务日志的监控预警、中间件的监控预警。让开发能实时了解机器和服务的状态。
2、高可用性
缓存:包括前端缓存,本地缓存,Redis缓存(降低IO);
异步化:主要体现在用户获取缓存之后,先写MQ,再通过MQ写数据库,降低直接对数据库的操作,提高并发量和稳定性(降低IO和CPU);
分布式:主要是把服务微服务化,可以通过docker快速扩缩容,当然微服务之后,需要进行服务治理保证服务的稳定可靠;
3、高并发
高并发就是对流量的控制;
前端流量控制
页面缓存
在前端,页面显示分为静态文件+部分后台数据,为了加快页面的反应和避免白屏现象,建议采用前端页面的缓存,针对前端页面,我们采用方式:1、从缓存中获取页面信息,如果数据存在,则页面进行渲染;2、异步请求后台数据,更新后台数据,并设置缓存5分钟左右;
后端处理
在秒杀的过程中,会先读取商品信息,然后再进行库存处理;首先为了提高并发度,商品数据和库存数据会进行存入Redis中;
商品缓存
商品缓存时会存在下面几个问题
1. 缓存穿透
2. 缓存击穿
3. 缓存雪崩
解决上面缓存问题代码
package com.newretail.account.service;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class CacheService {
@Autowired
private RedisTemplate redisTemplate;
private Cache<String, Object> dataCache = CacheBuilder.newBuilder() // 使用 CacheBuilder 创建
.expireAfterAccess(Duration.ofSeconds(30)) // 设置过期时间,在每次访问的半小时后过期,(再次访问则重新等待半小时)
// .expireAfterWrite(Duration.ofHour(2)) 在每次写入缓存2小时后过期
.maximumSize(10240) // 缓存最大数量
.concurrencyLevel(4) // 指定并发修改的数量,默认4
.initialCapacity(2048) // 设置初始化大小,避免经常扩容
.build(); // 无参的 build 方法创建 Cache
private Map<String, Object> locks = new ConcurrentHashMap<>();
private AtomicBoolean casLock = new AtomicBoolean(false);
public <T> List<T> getCache(String key, DBData<T> dbUtils, int expire) {
List<T> data = getCacheData(key);
if (data == null) {
// 先获取指定key的锁,在获取前,先得加锁,保证每个key只能获取一个Key
Object lock = null;
//此处用cas保证只有一个线程进来,主要原因是获取key锁都是内存操作,时间短
if (casLock.compareAndSet(false, true)) {
lock = locks.get(key);
if (lock == null) {
lock = new Object();
locks.put(key, lock);
}
casLock.set(false);
}
synchronized (lock) {
//DCL检查,保证获取锁的线程直接读取Redis的数据,不要执行DB操作
data = getCacheData(key);
if (data == null) {
data = dbUtils.getDBData();
setCacheData(key, expire, data);
}
}
// 清除内存,防止内存溢出,释放的时候,可能会有使一个key有多个锁,但是缓存中已经有了数据,不会有大面积请求请求到数据库
locks.remove(key);
}
return data;
}
private <T> List<T> getCacheData(String key) {
List<T> localCache = (List<T>) dataCache.getIfPresent(key);
if (localCache != null) {
return localCache;
}
List<T> redisCache = (List<T>) redisTemplate.opsForValue().get(key);
if (redisCache != null) {
dataCache.put(key, redisCache);
return redisCache;
}
return null;
}
private void setCacheData(String key, int expire, List cacheData) {
redisTemplate.opsForValue().set(key, cacheData, expire, TimeUnit.SECONDS);
dataCache.put(key, cacheData);
}
}
interface DBData<T> {
List<T> getDBData();
}
4. 数据一致性
具体使用可以参考:zhuanlan.zhihu.com/p/450576104
扣减库存方式
扣减库存方式:预订单扣减和支付扣减。
预订单扣减:用户下单之后立马扣减掉库存,一般会设置支付时间,针对秒杀系统,支付时间设置的较短,建议5分钟,5分钟之后,用户未付款,一般库存会换回到商品库存中;
支付扣减:是用户在支付的时候进行扣减库存,这种方式不会涉及到库存退还的逻辑,但是用户体验不太好,在用户秒杀完之后,进行付款时,会提示用户库存不足,同时也会产生大量的待付款的订单,形成一堆无效的数据;
公司采用预订单扣减库存。
怎么保证秒杀不超卖,不少卖
主要采用Redis TCC 和 异步消息的方式,具体步骤如下
1、设置活动库存的时候,基于商品ID和活动ID+固定前缀,组成库存的Redis key。
2、在用户下单时,先生成订单id,通过Lua脚本执行下面命令 DECR 商品库存数,Redis添加一条订单+商品的锁库存数,同时把锁的时间加到value中;
3、把预订单的所有信息发送到MQ中,如果MQ返回失败,则基于锁的数量INCR库存数,并清理掉锁记录数;
4、订单服务接受MQ的消息,写入数据库,然后清理Redis中锁的数据;
5、定时任务定时扫描Redis中锁的数据,如果锁的时间超过5分钟,再判断锁的记录在订单服务中是否有记录,如果有,则清理redis锁记录,如果在订单服务不存在,则把锁的数量还回Redis中;
针对扣减库存还有下面几点可以优化:
1、增加队列模式;
2、本地缓存模式,本地缓存数据加上一定Buff,先检查本地库存是否满足,再扣减Redis中的库存数;
分布式锁
为了方式重复请求,主要有下面几步
1、前端做按钮重复点击过滤,如:1秒钟最多点击1次;
2、秒杀系统通过分布式锁做请求过滤,锁的主键为:用户ID,商品ID,活动ID,下单数量,1s钟只有一个请求可以到下游,具体做法可以使用Redission实现;
3、订单服务消费消息的Exactly-Once
数据库方案:主要是通过事务保证消息只被消费一次,help.aliyun.com/zh/apsaramq…
Redis方案:
具体实现可以参考:jaskey.github.io/blog/2020/0…
公司商城秒杀系统的实现
针对茅台秒杀的相关说明
- 茅台现在采用的是预约制;
- 茅台是有利可图;
- 茅台的请求很多都是爬虫脚本;
所有茅台系统的实现大致可以分为下面几个方向:
- 加强黑名单机制,目前最严厉的是,如果每秒收到用户10个请求,则把用户的账号拉入黑名单;
- 控制虚拟手机号的注册,增加创造用户的成本;
- 因为是预约制,即可以知道可以抢到用户数,把可中签的用户加入内存中;
- 数据请求加密,不定期调整加密规则;
- 通过限流和waf拦截重复无效的用户,
目前公司用4台4核8G的机器支持茅台业务,茅台最高QPS 20万;
怎么发现热点
1、通过Redis命令查找热点key
2、实现Redis代理,进行统计热点key。热key可参考zhuanlan.zhihu.com/p/65790148
秒杀处理方法
- 限流:抛掉过多的无用流量,获取部分有效流量。限流分为下面几种方式:
- 前端限流,控制每秒客户端发送的请求数(一般控制每秒只能发送1-2个请求即可);
- Nginx限流、LBS限流或网关服务限流,降低到服务端的流量;
- 服务端限流,主要是用Sentinel(漏斗和令牌)或者其他的限流工具;
- Waf第三方服务;
- 黑名单机制;
- 流量分发(路由):流量根据机器的负载把流量近似公平的分派到各服务;
- 动态扩缩容:通过更多机器负载流量(需要考虑成本,最好设置最大扩容机器数)
- 服务降级:针对所有服务设置最大超时时间,针对核心业务制定好降级方案;