1. 在GoodsController中定義seckill方法對秒殺請求進(jìn)行處理,在處理的時候需要進(jìn)行一些判斷
//執(zhí)行秒殺
@PostMapping("/seckill/goods/{random}/{id}")
public @ResponseBody ReturnObject seckill(@PathVariable("random") String random,@PathVariable("id") Integer id){
ReturnObject returnObject = new ReturnObject();
return returnObject;
}
2. 請求參數(shù)random合法性驗證,我們這里采用的是長度判斷,有些公司將random的某個位置值固定,判斷是否為那個值
//1.random參數(shù)合法性驗證,我們這里采用的是長度判斷,有些公司將random的某個位置值固定,判斷是否為那個值
if(random.length() != 36){
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("請求參數(shù)有誤");
return returnObject;
}
3. 根據(jù)商品id從Redis中查詢出緩存的商品,判斷請求參數(shù)random和商品的randomName是否匹配
//2.根據(jù)商品id從Redis中查詢出緩存的商品,判斷請求參數(shù)random和商品的randomName是否匹配
String goodsJSON = redisTemplate.opsForValue().get(Constants.REDIS_GOODS+id);
Goods goods = JSONObject.parseObject(goodsJSON,Goods.class);
if (!random.equals(goods.getRandomname())){
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("請求參數(shù)有誤");
return returnObject;
}
4. 為了保險起見,我們再次驗證一下是否在秒殺時間內(nèi)(這步可以省略)
這里既沒有操作磁盤,也沒有操作數(shù)據(jù)庫,也沒有走網(wǎng)絡(luò),所以不會對性能產(chǎn)生影響
//3.為了保險起見,我們再次驗證一下是否在秒殺時間內(nèi)
Long currentTime = System.currentTimeMillis();
Long startTime = goods.getStarttime().getTime();
Long endTime = goods.getEndtime().getTime();
if(currentTime < startTime){
//秒殺尚未開始
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("秒殺尚未開始");
return returnObject;
}else if(currentTime > endTime){
//秒殺已經(jīng)結(jié)束
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("秒殺已經(jīng)結(jié)束");
return returnObject;
}else{
//如果秒殺已經(jīng)開始,處理業(yè)務(wù)繼續(xù)寫在這里
return returnObject;
}
5. 如果已經(jīng)開始秒殺驗證商品是否已經(jīng)賣光
需求:
如果商品已經(jīng)賣光,那么提示用戶,不能參與秒殺了
常規(guī)思路
● 直接查詢數(shù)據(jù)庫中商品的庫存,如果直接操作數(shù)據(jù)庫,秒殺場景,高并發(fā)大流量會給數(shù)據(jù)庫帶來很大的壓力。
● 從Redis中緩存的商品信息中獲取,但是后續(xù)秒殺結(jié)束后,涉及對庫存做修改,操作Redis的商品信息比較麻煩。另外,如果我們5秒緩存預(yù)熱一次,數(shù)據(jù)庫中商品的庫存還沒有修改,會被再次把數(shù)據(jù)庫中的庫存更新到Redis中。
解決方案
所以我們在緩存預(yù)熱的時候,直接將商品的庫存單獨存放到Redis中。并且這個信息需要在緩存預(yù)熱的時候生成,而且只能生成一次,因為我們減庫存我的時候,也是操作Redis,數(shù)據(jù)庫暫時不會變,如果每5秒初始化一次,那么會將數(shù)據(jù)庫的原始庫存又初始化到Redis中。
設(shè)置值的時候使用setIfAbsent方法
如果key不存在,那么設(shè)置值,如果已經(jīng)存在,不對其進(jìn)行設(shè)置值了
? 在15-seckill-service緩存預(yù)熱的定時任務(wù)中緩存商品庫存
Key的格式 redis:store:商品id Value的值:就是商品的庫存
/**
* 把數(shù)據(jù)庫中商品的庫存也預(yù)熱到Redis
* 注意:這里只能放一次,因為我們減庫存我的時候,也是操作Redis,數(shù)據(jù)庫暫時不會變
* 如果每5秒初始化一次,那么會將數(shù)據(jù)庫的原始庫存又初始化到Redis中
* setIfAbsent:如果key不存在,那么設(shè)置值,如果已經(jīng)存在,不對其進(jìn)行設(shè)置值了
*/
redisTemplate.opsForValue()
.setIfAbsent(Constants.REDIS_STORE + goods.getId(),String.valueOf(goods.getStore()));
? 在15-seckill-interface的Constants常量類下定義商品庫存key的前綴
/**
* 定義Redis中商品庫存的key的前綴
* Redis中存放商品庫存的格式:redis:goods:商品id
*/
public static final String REDIS_STORE = "redis:store:";
? 重新運行15-seckill-service,通過Redis DeskTop Manager查看Redis數(shù)據(jù)

? 在GoodsControll編寫驗證商品是否賣光代碼
//4.驗證商品是否已經(jīng)賣光了
//根據(jù)商品id,從Redis中獲取商品庫存
String redisStore = redisTemplate.opsForValue().get(Constants.REDIS_STORE + id);
//判斷是否為空 如果不為空將redis存放的庫存轉(zhuǎn)換為整形
Integer store = StringUtils.isEmpty(redisStore)? 0 :Integer.valueOf(redisStore);
//其實不會出現(xiàn)小于0的情況
if(store <= 0 ){
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("來晚了,商品已經(jīng)搶光了");
return returnObject;
}
為了對String操作更加方便,在15-seckill-web中引入commons-lang的依賴
<!--對常用類操作的增強包-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>
6. 驗證該用戶是否已經(jīng)秒殺過該商品
需求
同一件商品,同一個用戶只能秒殺一次
常規(guī)思路
去數(shù)據(jù)庫訂單表中查詢,是否有用戶對該商品的下單信息,但是秒殺場景,高并發(fā)大流量下,會給數(shù)據(jù)庫帶來很大的壓力
解決方案
我們這里還是查詢采用Redis,如果用戶秒殺了該商品,那就將用戶信息及商品信息組合放到Redis中,生成一條秒殺記錄,然后再秒殺的時候,從Redis中取數(shù)據(jù)進(jìn)行判斷
格式:redis:buy:id:uid
? 在15-seckill-interface的Constants常量中添加用戶是否購買過商品的key的前綴
/**
* 定義Redis中用戶是否買過該商品的key的前綴
* Redis中存放用戶是否買過該商品的格式:redis:buy:商品id:用戶id
*/
public static final String REDIS_BUY = "redis:buy:";
? 在15-seckill-web的GoodsController中編寫驗證是否買過該商品的代碼
//5.驗證用戶是否買過該商品
//假設(shè)用戶的id為888888,實際開發(fā)的使用用戶的id可以從session中獲取
Integer uid = 888888;
String redisBuy = redisTemplate.opsForValue().get(Constants.REDIS_BUY + id +":"+ uid);
//這里我們不需要關(guān)心redisBuy中放了什么,只要不為空,就說明用戶買個該商品
if(StringUtils.isNotEmpty(redisBuy)){
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("您已經(jīng)秒殺過該商品了,請換個商品秒殺");
return returnObject;
}
7. 限流
需求
在秒殺場景中,為每一個商品限制最大的參與搶購的人數(shù)為10w
不能為所有商品整體一個限流,否則會不平衡的問題,很多人都去秒殺一件商品,但是另一件商品在秒殺的時候,被限制了,誤殺!
實現(xiàn)方式
一般有專門的限流算法
我們使用Redis的List類型或者Redis計數(shù)器實現(xiàn)
如果用戶參與秒殺,向Redis的List中放一條記錄,然后判斷List的長度,Redis格式: redis:limit:商品id
? 在15-seckill-interface的Constants類中,添加限流最大值以及商品秒殺限流key的前綴常量
//商品限流最大值
public static final int MAX_LIMIT = 100000;
/**
* 定義Redis中商品秒殺限流key的前綴
* Redis中存放當(dāng)前商品的流量訪問值的格式:redis:limit:商品id
*/
public static final String REDIS_LIMIT = "redis:limit:";
? 在15-seckill-web的GoodsController中編寫限流代碼
//6.限流
//從Redis中查詢出當(dāng)前商品的訪問量
Long currentSize = redisTemplate.opsForList().size(Constants.REDIS_LIMIT + id);
if(currentSize > Constants.MAX_LIMIT){
//超過最大限流值,拒絕訪問
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("服務(wù)器繁忙,請稍后再試!~");
return returnObject;
}else{
//可以繼續(xù)執(zhí)行秒殺
// 先向Redis的限流List中放一條數(shù)據(jù) 返回放完數(shù)據(jù)之后List的長度
Long afterPushSize = redisTemplate.opsForList().leftPush(Constants.REDIS_LIMIT + id,String.valueOf(uid));
/*放完元素之后再次判斷List的長度是否大于限流值
主要處理多線程情況下,很多線程都滿足限流條件,都向Redis的List添加元素,避免List元素超出限流值
*/
if(afterPushSize >Constants.MAX_LIMIT){
redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + id);
//超過最大限流值,拒絕訪問
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("服務(wù)器繁忙,請稍后再試!~");
return returnObject;
}
}
8. 秒殺
? 減庫存
需求
秒殺結(jié)束后,將商品的庫存信息減1
常規(guī)的方式
減庫存是直接操作數(shù)據(jù)庫,高并發(fā)大流量的秒殺場景下,會給數(shù)據(jù)庫瞬間帶來極大的壓力,可能數(shù)據(jù)庫無法支撐(單臺MySQL并發(fā)能力700左右,單臺Redis并發(fā)能力5w左右)
解決方案
所以減庫存在Redis中減
A、 15-seckill-web減庫存代碼
//減庫存
Long leftStore = redisTemplate.opsForValue().decrement(Constants.REDIS_STORE +"id",1);
? 下訂單(僅僅是將訂單發(fā)送給MQ)
需求
秒殺之后,僅僅將訂單發(fā)送給MQ,暫時不想數(shù)據(jù)庫訂單表中插入數(shù)據(jù)
常規(guī)的做法
直接是向數(shù)據(jù)庫中插入訂單信息
秒殺場景,可能有很多訂單可以插入到數(shù)據(jù)庫,而且主要是瞬間的操作,例如:1s,5s內(nèi)向數(shù)據(jù)庫插入10w條數(shù)據(jù)。
所以下單的時候不能直接操作數(shù)據(jù)庫
解決方案
我們采用MQ,進(jìn)行異步下單
同步是阻塞的,是需要等結(jié)果的,是可以拿到結(jié)果的
異步是非阻塞的,不需要等結(jié)果,但是有可能馬上拿不到結(jié)果
讓MQ接收瞬間的巨大的下單請求,但并不是馬上瞬間處理完畢,而是一個個處理,插入數(shù)據(jù)庫的頻率的降低。這個頻率的降低,我們叫做流量削峰,將單位時刻內(nèi),對數(shù)據(jù)庫的操作降緩
MQ處理完畢之后,僅僅是將消息發(fā)送到了ActiveMQ的消息隊列中,并沒有真正的同步數(shù)據(jù)庫,所以不能馬上給前臺結(jié)果,那么這個時候我們可以告訴前臺頁面一個中間結(jié)果,秒殺請求提交成功,正在處理……或一個圖片轉(zhuǎn)動
A、 在15-seckill-web的pom.xml文件添加ActiveMQ起步依賴
<!--SpringBoot集成ActiveMQ的起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
B、 在15-seckill-web的核心配置文件中配置ActiveMQ信息
#配置activemq的連接信息
spring.activemq.broker-url=tcp://192.168.235.128:61616
# 用戶名
spring.activemq.user=system
# 密碼
spring.activemq.password=123456
#目的地
spring.jms.template.default-destination=seckillQueue
C、 在15-seckill-web的GoodsController類中注入JmsTemplate
@Autowired
private JmsTemplate jmsTemplate;
D、 在15-seckill-web的GoodsController類中編寫下訂單代碼
//7.減庫存
Long leftStore = redisTemplate.opsForValue().decrement(Constants.REDIS_STORE +id,1);
//8.下單到MQ
if(leftStore >= 0){
//可以秒殺,執(zhí)行下單操作
//標(biāo)記用戶已經(jīng)買過該商品
redisTemplate.opsForValue().set(Constants.REDIS_BUY + id +":" +uid,String.valueOf(uid));
//創(chuàng)建訂單對象
Orders orders = new Orders();
orders.setBuynum(1);
orders.setBuyprice(goods.getPrice());
orders.setCreatetime(new Date());
orders.setGoodsid(id);
orders.setOrdermoney(goods.getPrice().multiply(new BigDecimal(1)));
orders.setStatus(1);//待支付
orders.setUid(uid);
//將訂單對象轉(zhuǎn)換為json字符串
String ordersJSON = JSONObject.toJSONString(orders);
//通過JmsTemplate向ActiveMQ發(fā)送消息
jmsTemplate.send(new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(ordersJSON);
}
});
returnObject.setErrorCode(Constants.ONE);
returnObject.setErrorMessage("秒殺請求提交成功,正在處理....");
return returnObject;
}else{
//不可以賣了,不能執(zhí)行下單操作
/*
此時Redis中的商品庫存可能已經(jīng)減成負(fù)數(shù)了,但是對我們業(yè)務(wù)的處理沒有任何影響
但為了保持?jǐn)?shù)據(jù)的一致性,我們將值再恢復(fù)一下
*/
redisTemplate.opsForValue().increment(Constants.REDIS_STORE + id,1);
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("來晚了,商品已經(jīng)搶光了");
return returnObject;
}
E、在15-seckill-web的seckill.js的execseckill函數(shù)中處理返回信息
//執(zhí)行秒殺請求
execSeckill:function (random,id) {
$.ajax({
//url格式: /15-seckill-web/seckill/goods/Ffdaskfjkadlsjklfa/1
url: seckillObj.url.seckillURL() + random +"/" +id,
type:"post",
dataType:"json",
success:function (rtnMessage) {
//處理響應(yīng)結(jié)果
if(rtnMessage.errorCode == 1){
//秒殺成功,已經(jīng)下單到MQ,返回中間結(jié)果 可以做動畫處理
$("#seckillTip").html("<span style='color:red;'>"+ rtnMessage.errorMessage +"</span>");
//接下來再發(fā)送一個請求獲取最終秒殺的結(jié)果
}else{
//秒殺失敗 展示失敗信息
$("#seckillTip").html("<span style='color:red;'>"+ rtnMessage.errorMessage +"</span>");
}
}
});
}
F、 啟動ActiveMQ,Redis,MySQL,15-seckill-service,15-seckill-web測試


? 在15-seckill-service中的RedisTask中同步MySQL數(shù)據(jù)庫庫存
/**
* 每3秒同步一次Redis中的庫存到數(shù)據(jù)庫
*/
@Scheduled(cron = "0/3 * * * * *")
public void syncRedisStoreToDB(){
System.out.println("同步Redis中的庫存到數(shù)據(jù)庫...........");
//1.查詢出所有秒殺商品在Redis中的庫存值
Set<String> keys = redisTemplate.keys(Constants.REDIS_STORE + "*");
for (String key : keys) {
//根據(jù)Redis的商品庫存key,獲取商品的庫存
int store = Integer.valueOf(redisTemplate.opsForValue().get(key));
//獲取商品的id 在Redis中存放商品庫存的格式 redis:store:id
int goodsId = Integer.valueOf(key.split(":")[2]);
//同步到數(shù)據(jù)庫
Goods goods = new Goods();
goods.setId(goodsId);
goods.setStore(store);
goodsMapper.updateByPrimaryKeySelective(goods);
}
}
9. 異步下單的處理
需求
將MQ中的訂單同步到數(shù)據(jù)庫
實現(xiàn)思路
● 在15-seckill-service中使用異步接收消息的方式對秒殺的訂單消息進(jìn)行消費
● 為了方便對事務(wù)的處理,我們在消息消費者M(jìn)yMessageListener中不直接調(diào)用Mapper,而是調(diào)用訂單的Service
● 如果下單成功
在Service中將秒殺的最終結(jié)果返回給前臺頁面,這里存在一個問題,就是如何將秒殺的結(jié)果響應(yīng)給前臺頁面?
傳統(tǒng)的做法,前臺頁面可以直接查詢數(shù)據(jù)庫的訂單表,獲取最終的秒殺結(jié)果,但是會對數(shù)據(jù)庫造成壓力,我們這里借助第三方Redis,將返回的結(jié)果保存到Redis中,然后讓前臺頁面到Redis中進(jìn)行查詢。
● 如果下單失敗
在Service層中拋出異常,在MyMessageListener中捕獲異常,對之前做的處理進(jìn)行恢復(fù),主要包括庫存恢復(fù)、購買標(biāo)記、限流列表中刪除一個元素
恢復(fù)的操作我們也專門在Service中封裝方法
? 在15-seckill-service中添加ActiveMQ相關(guān)依賴
<!--SpringBoot集成ActiveMQ的起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
? 在15-seckill-service中的pom.xml文件中添加ActiveMQ配置信息
#配置activemq的連接信息
spring.activemq.broker-url=tcp://192.168.235.128:61616
spring.activemq.user=system
spring.activemq.password=123456
#目的地
spring.jms.template.default-destination=seckillQueue
# 消息發(fā)送模式 true發(fā)布訂閱 false點對點 默認(rèn)false點對點
spring.jms.pub-sub-domain=false
# SpringBoot 2.1.3之后需要配置
spring.jms.cache.enabled=false
? 從13-activemq-boot-receiver-async-02中拷貝ActiveMQ異步接收的代碼config和listener目錄下的內(nèi)容

? ActiveMQConfig代碼(不需要修改)
@Configuration//相當(dāng)于applicationContext-jms.xml文件
public class ActiveMQConfig {
@Autowired
private ActiveMQConnectionFactory connectionFactory;
@Autowired
private MyMessageListener myMessageListener;
@Value("${spring.jms.template.default-destination}")
private String destination;
@Value("${spring.jms.pub-sub-domain}")
private boolean pubSubDomain;
@Bean //@Bean注解就相當(dāng)于配置文件的bean標(biāo)簽
public DefaultMessageListenerContainer defaultMessageListenerContainer(){
DefaultMessageListenerContainer listenerContainer = new DefaultMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory);
listenerContainer.setDestinationName(destination);
listenerContainer.setMessageListener(myMessageListener);
//設(shè)置消息發(fā)送模式方式為發(fā)布訂閱
listenerContainer.setPubSubDomain(pubSubDomain);
return listenerContainer;
}
}
? 修改15-seckill-service中的MyMessageListener消費消息
@Component
public class MyMessageListener implements MessageListener{
@Autowired
private OrdersService ordersService;
public void onMessage(Message message) {
if(message instanceof TextMessage){
try {
String ordersJSON = ((TextMessage) message).getText();
System.out.println("SpringBoot監(jiān)聽器異步接收到的消息為:" + ordersJSON);
Orders orders = JSONObject.parseObject(ordersJSON,Orders.class);
try {
//接收到消息,下訂單
ordersService.addOrders(orders);
} catch (Exception e) {
e.printStackTrace();
//下單失敗了,要將之前的一些處理恢復(fù)一下
ordersService.processException(orders);
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
? 在15-seckill-interface的com.bjpowernode.seckill.service包下創(chuàng)建訂單接口OrdersService
public interface OrdersService {
/**
* 下訂單
*/
int addOrders(Orders orders);
/**
* 下單失敗對異常的處理
*/
void processException(Orders orders);
}
? 在15-seckill-service的com.bjpowernode.seckill.service.impl包下中創(chuàng)建訂單接口實現(xiàn)類OrdersServiceImpl
@Service
public class OrdersServiceImpl implements OrdersService{
@Autowired
private OrdersMapper ordersMapper;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Transactional
@Override
public int addOrders(Orders orders) {
int addRow = ordersMapper.insertSelective(orders);
if(addRow >0){
/*下單成功,告知前臺秒殺最終結(jié)果,我們這是service項目,由消息消費者調(diào)用,不能
直接和前臺打交道,所以需要前臺重新發(fā)送請求,去數(shù)據(jù)庫訂單表中查詢結(jié)果,但是這樣
對數(shù)據(jù)庫帶來壓力,所以我們將秒殺最終結(jié)果放到Redis中,然后前臺頁面去Redis中查詢
*/
//用我們自定義我的RTO對象封裝秒殺結(jié)果
ReturnObject returnObject = new ReturnObject();
returnObject.setErrorCode(Constants.ONE);
returnObject.setErrorMessage("秒殺成功");
returnObject.setData(orders);
String returnJSON = JSONObject.toJSONString(returnObject);
redisTemplate.opsForValue().set(Constants.REDIS_RESULT +
orders.getGoodsid() +":" + orders.getUid(),returnJSON);
//當(dāng)前這個人秒殺全部結(jié)束,應(yīng)該把當(dāng)前這個人從限流列表中刪除,讓后面的人再進(jìn)來秒殺
redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + orders.getGoodsid());
}else{
//下單失敗,拋出運行時異常
throw new RuntimeException("秒殺下單失敗");
}
return addRow;
}
/**
下單失敗之后,進(jìn)行之前處理數(shù)據(jù)的恢復(fù)
*/
@Override
public void processException(Orders orders) {
// 1.庫存恢復(fù)
redisTemplate.opsForValue().increment(Constants.REDIS_STORE + orders.getGoodsid(),1);
//2.購買標(biāo)記清除
redisTemplate.delete(Constants.REDIS_BUY + orders.getGoodsid() +":" + orders.getUid());
// 3.限流列表中刪除一個元素
redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + orders.getGoodsid());
//4.將失敗信息放到Redis中,便于前臺頁面再次獲取
ReturnObject returnObject = new ReturnObject();
returnObject.setErrorCode(Constants.ZERO);
returnObject.setErrorMessage("秒殺失敗");
returnObject.setData(orders);
String returnJSON = JSONObject.toJSONString(returnObject);
redisTemplate.opsForValue().set(Constants.REDIS_RESULT +
orders.getGoodsid() + ":" + orders.getUid(), returnJSON);
}
}
? 在15-seckill-interface的Constants類中添加存放最終秒殺結(jié)果Key的前綴
/**
* 定義Redis中商品秒殺秒殺結(jié)果key的前綴
* Redis中存放當(dāng)前商品的流量訪問值的格式:redis:result:商品id:用戶id
*/
public static final String REDIS_RESULT = "redis:result:";
? 在15-seckill-service的Application類上開啟事務(wù)
@EnableTransactionManagement
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
10. 庫存超賣的解讀
參照面試題11-Summary\互聯(lián)網(wǎng)金融項目-面試.docx