秒杀项目笔记
第二章,项目回归

ItemController: 商品相关Controller
OrderController:交易相关Controller
UserController:用户相关Controller
OrderService:下单交易
UserService:获取用户信息、注册、登录
ItemService:创建商品、商品列表浏览、商品详情浏览、商品减库存、商品加销量
PromoService:获取秒杀活动商品信息
PromoDoMapper:秒杀活动数据表操作
ItemDoMapper:商品数据操作
ItemStockDoMapper:库存数据表操作
OrderDoMapper:订单数据表操作
UserDoMapper:用户数据表操作
UserPasswordMapper:用户密码操作
第三章 云端部署,修改Server配置
本地在项目根目录下使用mvn clean package 打包生成miaosha.jar文件
将jar包上传到服务端并编写额外的application.properties配置文件
编写deploy.sh文件启动对应的项目 java命令启动,设置JVM初始和最大内存为2048m,2个g大小,设置JVM初始新生代和最大新生代大小为1024m,设置成一样的目的是为了减少扩展jvm内存过程中向操作系统索要内存分配的消耗,
-spring config addition-location=指定额外的配置文件地址
nohub java -Xms2048m -Xmx2048m -XX:NewSize=1024m -XX:MaxNewSize=1024m -jar miaosha.jar --spring.config.addition-location=/var/www/miaosha/application.properties
Spring-configuration-metadata.json
server.tomcat.accept-count:等待队列长度,默认100 server.tomcat.max-connections:最大可被连接数,默认10000 server.tomcat.max-threads:最大工作线程数,默认200 server.min-spare-threads:最小工作线程数,默认是10 # 默认配置下,请求超过1000后出现拒绝连接情况 # 默认配置下,触发的请求超过200+100后拒绝连接 最大工作线程数+等待队列的长度
# 修改默认参数值 4核8G机器 server.tomcat.max-threads:800 server.min-spare-threads:100
keepAliveTimeOut:多少毫秒后不响应的断开Keepalive
maxKeepAliveRequests:多少次请求后Keepalive断开失效
定制化:使用WebServerFactoryCunstomizer 定制化内嵌Tomcat
//当Spring容器内没有TomcatEmbeddedServletContainerFactory这个bean时,会吧此bean加载进spring容器中
@Component
public class WebServerConfiguration implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory configurableWebServerFactory) {
//使用对应工厂类提供给我们的接口定制化我们的tomcat connector
((TomcatServletWebServerFactory)configurableWebServerFactory).addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override
public void customize(Connector connector) {
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
//定制化keepalivetimeout,设置30秒内没有请求则服务端自动断开keepalive链接
protocol.setKeepAliveTimeout(30000);
//当客户端发送超过10000个请求则自动断开keepalive链接
protocol.setMaxKeepAliveRequests(10000);
}
});
}
} 第四章,单机容量问题,水平扩展方案引入
表象:单机cpu使用率增高,memory占用增加,网路带宽使用增加
cpu us:用户空间的cpu使用情况(用户层代码)
Cpu sy:内核空间的cpu使用情况(系统调用)
Load average:1.5,15分钟load平均值,跟这核数增加,0代表通常,1代表打满,1+代表等待阻塞
memory:free空闲内存,used使用内存
nginx 动静分离设置
会话管理
- 基于token传输类似sessionid:java代码session实现迁移到redis
String uuidToken = UUID.randomUUID().toString();
uuidToken = uuidToken.replace("-","");
//建立token和用户登陆态之间的联系
redisTemplate.opsForValue().set(uuidToken,userModel);
//设置超时时间 1hour
redisTemplate.expire(uuidToken,1, TimeUnit.HOURS);
//下发了token
return CommonReturnType.create(uuidToken); 前端代码存储uuidToken
Login.html
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://"+g_host+"/user/login",
data:{
"telphone":$("#telphone").val(),
"password":password
},
xhrFields:{withCredentials:true},
success:function(data){
if(data.status == "success"){
alert("登陆成功");
var token = data.data;
//存储token
window.localStorage["token"] = token;
//从定向到listitem.html
window.location.href="listitem.html";
}else{
alert("登陆失败,原因为"+data.data.errMsg);
}
},
error:function(data){
alert("登陆失败,原因为"+data.responseText);
}
});
return false;
}); getItem.html
var token = window.localStorage["token"];
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"http://"+g_host+"/order/generatetoken?token="+token,
data:{
"itemId":g_itemVO.id,
"promoId":g_itemVO.promoId,
"verifyCode":$("#verifyContent").val()
}, OrderController.java--> createOrder Method
String token = httpServletRequest.getParameterMap().get("token")[0];
if(StringUtils.isEmpty(token)){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
}
//获取用户的登陆信息
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if(userModel == null){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
} 基于cookie传输的session:java tomcat容器session的实现
// 1.引入依赖 <!--引入依赖--> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.0.5.RELEASE</version> </dependency> // 2.修改Redis配置文件 保存到的Session 过期时间 @Component @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600) //3.保存登录状态 public class RedisConfig { } this.httpServletRequest.getSession().setAttribute("IS_LOGIN",true); this.httpServletRequest.getSession().setAttribute("LOGIN_USER",userModel);
缺点:企业级应用,不光支持html,还要支持Android、IOS 网络情况下cookie的规则会不会改变,还有Cookie被客户端禁用的可能。
第五章 查询优化技术之多级优化
缓存设计
- 用快速缓存设备,用内存
- 将缓存推到离用户最近的地方
- 脏缓存清理(关键型数据必须存储在数据库中,将查询的热点数据放入缓存,注意一致性问题)
多集缓存
- redis缓存<单机版、Sentinal哨兵模式、集群Cluster模式>
- 热点内存本地缓存
- nginx proxy cache缓存
- nginx lua 缓存
商品详情动态内容实现。
ItemController --->getItem()
//商品详情页浏览
@RequestMapping(value = "/get",method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id")Integer id){
ItemModel itemModel = null;
//根据商品的id到redis内获取
itemModel = (ItemModel) redisTemplate.opsForValue().get("item_"+id);
//若redis内不存在对应的itemModel,则访问下游service
if(itemModel == null){
itemModel = itemService.getItemById(id);
//设置itemModel到redis内
redisTemplate.opsForValue().set("item_"+id,itemModel);
redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES);
}
ItemVO itemVO = convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
} 本地热点缓存<VM虚拟机堆栈2G设置就是为了使用对象管理区间>
用于存放热点数据、脏读非常不敏感、内存可控
Guava Cache 类似于HashMap,可空值的大小和超时时间、可以配置lru清除策略、线程安全的
Map<Integer、ItemModel>
可以支持并发读写的hashMap、想到currentHashMap是基于段的处理方式去加速,在处理Put时写锁加上会对读锁性能有影响,而且可以设置key过期时间。
// 1.maven引入guava
<!--引入guava-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
//2.封装CacheService,完成对象的存储
//3.编写CacheService
//封装本地缓存操作类
public interface CacheService {
//存方法
void setCommonCache(String key,Object value);
//取方法
Object getFromCommonCache(String key);
}
//4.编写CacheServiceImpl
@Service
public class CacheServiceImpl implements CacheService {
private Cache<String,Object> commonCache = null;
@PostConstruct
public void init(){
commonCache = CacheBuilder.newBuilder()
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置缓存中最大可以存储100个KEY,超过100个之后会按照LRU的策略移除缓存项
.maximumSize(100)
//设置写缓存后多少秒过期
.expireAfterWrite(60, TimeUnit.SECONDS).build();
}
@Override
public void setCommonCache(String key, Object value) {
commonCache.put(key,value);
}
@Override
public Object getFromCommonCache(String key) {
return commonCache.getIfPresent(key);
}
}
//5.改造ItemService
//商品详情页浏览
@RequestMapping(value = "/get",method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id")Integer id){
ItemModel itemModel = null;
//先取本地缓存
itemModel = (ItemModel) cacheService.getFromCommonCache("item_"+id);
if(itemModel == null){
//根据商品的id到redis内获取
itemModel = (ItemModel) redisTemplate.opsForValue().get("item_"+id);
//若redis内不存在对应的itemModel,则访问下游service
if(itemModel == null){
itemModel = itemService.getItemById(id);
//设置itemModel到redis内
redisTemplate.opsForValue().set("item_"+id,itemModel);
redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES);
}
//填充本地缓存
cacheService.setCommonCache("item_"+id,itemModel);
}
ItemVO itemVO = convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
} 缺点:当数据更新时、本地热点缓存无能为力而且也有容量上的问题
nginx proxy cache缓存<有这个方案,但是提升不明显,弃用>
- nginx反向***前置
- 依靠文件系统存索引级的文件
- 依靠内存缓存文件地址
nginx lua
第六章 查询优化技术之页面静态化[动态请求加静态页面静态化]
第七章 交易优化技术之缓存库存[用缓存解决交易问题]

交易系统性能瓶颈
1.交易校验操作完全依赖数据库<发送了6次sql>
2.落单减库存操作
<update id="decreaseStock">
update item_stock
set stock = stock - #{amount}
where item_id = #{itemId} and stock >= #{amount} 有数据库行锁等待
</update> - 用户风控策略优化:策略缓存模型化
//1.通过缓存获取item模型 ItemService -->getItemByIdInCache()
@Override
public ItemModel getItemByIdInCache(Integer id) {
ItemModel itemModel = (ItemModel) redisTemplate.opsForValue().get("item_validate_"+id);
if(itemModel == null){
itemModel = this.getItemById(id);
redisTemplate.opsForValue().set("item_validate_"+id,itemModel);
redisTemplate.expire("item_validate_"+id,10, TimeUnit.MINUTES);
}
return itemModel;
}
//2.通过缓存获取user模型 UserService -->getUserByIdInCache()
@Override
public UserModel getUserByIdInCache(Integer id) {
UserModel userModel = (UserModel) redisTemplate.opsForValue().get("user_validate_"+id);
if(userModel == null){
userModel = this.getUserById(id);
redisTemplate.opsForValue().set("user_validate_"+id,userModel);
redisTemplate.expire("user_validate_"+id,10, TimeUnit.MINUTES);
}
return userModel;
}
//2.修改下单流程(直接查询数据库--->先查询缓存,在查询数据库)
//3.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
//ItemModel itemModel = itemService.getItemById(itemId);
ItemModel itemModel = itemService.getItemByIdInCache(itemId);
if(itemModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
}
UserModel userModel = userService.getUserByIdInCache(userId);
if(userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"用户信息不存在");
} - 活动校验策略优化:引入活动发布流程,模型缓存化,紧急下线能力
库存行锁优化
alter table item_stock add unique index item_id_index(itemid)
扣减库存缓存化
活动发布同步库存进库存
//1.活动发布 PromoService public void publishPromo(Integer promoId) { //通过活动id获取活动 PromoDO promoDO = promoDOMapper.selectByPrimaryKey(promoId); //验证活动是否存在 if(promoDO.getItemId() == null || promoDO.getItemId().intValue() == 0){ return; } // TODO 上下架商品操作... ItemModel itemModel = itemService.getItemById(promoDO.getItemId()); //将库存同步到redis内 redisTemplate.opsForValue().set("promo_item_stock_"+itemModel.getId(), itemModel.getStock()); }
下单交易减缓存库存<数据库记录不一致>
@Transactional public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException { //int affectedRow = itemStockDOMapper.decreaseStock(itemId,amount); long result = redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue() * -1); if(result >0){ //更新库存成功 return true; }else if(result == 0){ //打上库存已售罄的标识 redisTemplate.opsForValue().set("promo_item_stock_invalid_"+itemId,"true"); //更新库存成功 return true; }else{ //更新库存失败 increaseStock(itemId,amount); return false; } }异步消息扣减数据库内库存
异步消息队列rocketmq基于kafka改造的中间件,重试消息队列、延迟消息队列。
引入maven依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.3.0</version> </dependency>
Application.properties
mq.nameserver.addr=115.28.59.132:9876 mq.topicname=stock
MQProducer
// 1.MQ初始化 @PostConstruct public void init() throws MQClientException { //做mq producer的初始化 producer = new DefaultMQProducer("producer_group"); producer.setNamesrvAddr(nameAddr); producer.start(); } //2.同步扣减库存消息 //同步库存扣减消息 public boolean asyncReduceStock(Integer itemId,Integer amount) { Map<String,Object> bodyMap = new HashMap<>(); bodyMap.put("itemId",itemId); bodyMap.put("amount",amount); Message message = new Message(topicName,"increase", JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8"))); try { producer.send(message); } catch (MQClientException e) { e.printStackTrace(); return false; } catch (RemotingException e) { e.printStackTrace(); return false; } catch (MQBrokerException e) { e.printStackTrace(); return false; } catch (InterruptedException e) { e.printStackTrace(); return false; } return true; }
MQConsumer
@Component public class MqConsumer { private DefaultMQPushConsumer consumer; @Value("${mq.nameserver.addr}") private String nameAddr; @Value("${mq.topicname}") private String topicName; @Autowired private ItemStockDOMapper itemStockDOMapper; @PostConstruct public void init() throws MQClientException { consumer = new DefaultMQPushConsumer("stock_consumer_group"); consumer.setNamesrvAddr(nameAddr); consumer.subscribe(topicName,"*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { //实现库存真正到数据库内扣减的逻辑 Message msg = msgs.get(0); String jsonString = new String(msg.getBody()); Map<String,Object>map = JSON.parseObject(jsonString, Map.class); Integer itemId = (Integer) map.get("itemId"); Integer amount = (Integer) map.get("amount"); itemStockDOMapper.decreaseStock(itemId,amount); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); }
异步同步数据库的问题
- 异步消息发送失败
- 扣减操作执行失败
- 下单失败无法正确回补库存
第八章 交易优化技术之事务型消息[保证最终一致性的利器]
- 操作流水(stock_log表)
create table `stock_log`(
`stock_log_id` varchar(64) not null primary key,
`item_id` int(11) not null,
`amount` int(11),
`status` tiny int comment '1表示系统初始状态,2代表下单扣库存成功,3代表下单回滚'
) 初始化stock_log
1.下单之前加入库存流水Init状态
//初始化对应的库存流水
@Override
@Transactional
public String initStockLog(Integer itemId, Integer amount) {
StockLogDO stockLogDO = new StockLogDO();
stockLogDO.setItemId(itemId);
stockLogDO.setAmount(amount);
stockLogDO.setStockLogId(UUID.randomUUID().toString().replace("-",""));
stockLogDO.setStatus(1);
stockLogDOMapper.insertSelective(stockLogDO);
return stockLogDO.getStockLogId();
} LocalTransactionState的三种状态:COMMIT_MESSAGE、ROLLBACK_MESSAGE、UNKNOW
rocket mq提供的TransactionMQProducer API 执行流程:
- 先发送需要发送的消息到消息中间件broker,并获取到该message的transactionId。在第一次发送的时候,该消息的状态为LocalTransactionState.UNKNOW
- 处理本地事物。
- 根据本地事物的执行结果,结合transactionId,找到该消息的位置,在mq中标志该消息的最终处理结果。
上述:如果第三阶段出现异常或者网络原因,就是本地事务执行成功持久化到数据库中,但是在修改mq中消息状态出现异常的时候,这样就可以出现本地和mq的消息状态的不一致问题。或者说,所有的数据不一致问题。rocketmq都会定期通过TransactionMQProducer API初始化的时候,设置的TransactionCheckListener的的实现类的checkLocalTransactionState 方法检查本地消息的状态,根据本地状态修改mq的状态
第九章 流量削峰计数[削峰填谷之神操作]
- 掌握秒杀令牌的原理和使用方式
- 掌握秒杀大闸的原理和使用方式
- 掌握队列泄洪的原理和使用方式
抛缺陷
- 秒杀下单接口会被脚本不停的刷
- 秒杀验证逻辑和秒杀下单接口强关联,代码冗余度高
- 秒杀验证逻辑复杂,对交易系统产生无关联负载
秒杀令牌原理
- 秒杀接口需要依靠令牌才能进入
- 秒杀的令牌由秒杀活动模块负责生成
- 秒杀活动模块对秒杀令牌生成全权处理,逻辑收口
- 秒杀下单前需要先获得秒杀令牌
// 1.管理令牌生成
PromoService --> generateToken
//生成token并且存入redis内并给一个5分钟的有效期
//判断当前时间是否秒杀活动即将开始或正在进行
if(promoModel.getStartDate().isAfterNow()){
promoModel.setStatus(1);
}else if(promoModel.getEndDate().isBeforeNow()){
promoModel.setStatus(3);
}else{
promoModel.setStatus(2);
}
//判断活动是否正在进行
if(promoModel.getStatus().intValue() != 2){
return null;
}
//判断item信息是否存在
ItemModel itemModel = itemService.getItemByIdInCache(itemId);
if(itemModel == null){
return null;
}
//判断用户信息是否存在
UserModel userModel = userService.getUserByIdInCache(userId);
if(userModel == null){
return null;
}
String token = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,token);
redisTemplate.expire("promo_token_"+promoId+"_userid_"+userId+"_itemid_"+itemId,5, TimeUnit.MINUTES); //生成秒杀令牌
@RequestMapping(value = "/generatetoken",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType generatetoken(@RequestParam(name="itemId")Integer itemId,
@RequestParam(name="promoId")Integer promoId,
@RequestParam(name="verifyCode")String verifyCode) throws BusinessException {
//根据token获取用户信息
String token = httpServletRequest.getParameterMap().get("token")[0];
if(StringUtils.isEmpty(token)){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
}
//获取用户的登陆信息
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if(userModel == null){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"用户还未登陆,不能下单");
}
//通过verifycode验证验证码的有效性
String redisVerifyCode = (String) redisTemplate.opsForValue().get("verify_code_"+userModel.getId());
if(StringUtils.isEmpty(redisVerifyCode)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法");
}
if(!redisVerifyCode.equalsIgnoreCase(verifyCode)){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"请求非法,验证码错误");
}
//获取秒杀访问令牌
String promoToken = promoService.generateSecondKillToken(promoId,itemId,userModel.getId());
if(promoToken == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"生成令牌失败");
}
//返回对应的结果
return CommonReturnType.create(promoToken);
} 秒杀大闸
- 依靠秒杀令牌的授权原理定制化发牌逻辑,做到大闸功能
- 根据秒杀商品初始化库存颁发对应数量令牌,控制大闸数量<库存数量*5发放令牌>
- 用户风控策略前置到秒杀令牌发放
- 库存售罄判断前置到秒杀令牌发放中
抛出缺陷
- 浪涌流量涌入后系统无法应对
- 多库存,多商品等令牌限制能力弱
方案:队列泄洪策略
- 排队有时候比并发更加高效(例如redis单线程模型,innodb mutex key等)
- 依靠排队去限制并发流量
- 依靠排队和下游拥塞窗口程度调整队列释放流量大小
Redis为什么快?
内存级别数据库,
单线程操作,不会有线程内上下文内存上的开销
CreateOrder()
private ExecutorService executorService;
@PostConstruct
public void init(){
executorService = Executors.newFixedThreadPool(20);
}
//同步调用线程池的submit方法
//拥塞窗口为20的等待队列,用来队列化泄洪
Future<Object> future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//加入库存流水init状态
String stockLogId = itemService.initStockLog(itemId,amount);
//再去完成对应的下单事务型消息机制
if(!mqProducer.transactionAsyncReduceStock(userModel.getId(),itemId,promoId,amount,stockLogId)){
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下单失败");
}
return null;
}
});
本地 or 分布式
- 本地:将队列维护在本地内存中<JVM中,性能和高可用,缺点负载可能不均衡>
- 分布式:将队列设置到外部redis内<性能问题:发送任何请求都要发送网络IO、而且还有单点问题>
第十章 防刷限流计数[保护系统,避免过载]
- 掌握验证码生成与验证技术
- 掌握限流原理与实现
- 掌握防黄牛技术
验证码
- 包装秒杀令牌前置,需要验证码来错峰<使用户流量错峰>
限流目的
- 限流方案
- 限并发
- 令牌桶算法<应对突发流量>
- 漏桶算法<以固定的速率流入网络>
- 限流力度
- 接口维度
- 总维度
- 限流范围
- 集群限流:依赖redis或者其他的技术做统一计数器,往往会产生性能瓶颈
- 单机限流:负载均衡的前提下单机平均限流效果更好
限流代码实现:
Guava RateLimit
// orderController private RateLimiter orderCreateRateLimiter; if(order)
