電商專案day19(秒殺功能實現)
今日目標:
秒殺實現思路
實現秒殺下單功能
完成下單併發產生的訂單異常問題 超賣
完成高併發下使用者下單排隊和超限問題
一.秒殺的思路分析
1.需求分析:
所謂“秒殺”,就是網路賣家釋出一些超低價格的商品,所有買家在同一時間網上搶購的一種銷售方式。通俗一點講就是網路商家為促銷等目的組織的網上限時搶購活動。由於商品價格低廉,往往一上架就被搶購一空,有時只用一秒鐘。
秒殺一共有,兩種限制:庫存限制和時間限制
需求:
(1)商家提交秒殺商品申請,錄入秒殺商品資料,主要包括:商品標題、原價、秒殺價、商品圖片、介紹等資訊
(2)運營商稽核秒殺申請
(3)秒殺頻道首頁列出秒殺商品(進行中的)點選秒殺商品圖片跳轉到秒殺商品詳細頁。
(4)商品詳細頁顯示秒殺商品資訊,點選立即搶購實現秒殺下單,下單時扣減庫存。當庫存為 0 或不在活動期範圍內時無法秒殺。
(5)秒殺下單成功,直接跳轉到支付頁面(微信掃碼),支付成功,跳轉到成功頁,填寫收貨地址、電話、收件人等資訊,完成訂單。
(6)當用戶秒殺下單 5 分鐘內未支付,取消預訂單,呼叫微信支付的關閉訂單介面,恢復庫存。
資料庫表的分析:
秒殺表的資料庫表,我們不放在其他的商品表中,單獨設計一個表結構
tb_seckill_goods表結構
tb_seckill_order表主要是:秒殺成功後生成的訂單
我們分析什麼條件的商品的資料能夠在秒殺頁面展示?
稽核通過
有庫存
當前實現大於開始時間,並小於秒殺結束時間 即正在秒殺的商品
秒殺的實現思路分析:
秒殺技術實現核心思想是運用快取減少資料庫瞬間的訪問壓力!讀取商品詳細資訊時運用快取,當用戶點選搶購時減少快取中的庫存數量,當庫存數為 0 時或活動期結束時,同步到資料庫。 產生的秒殺預訂單也不會立刻寫到資料庫中,而是先寫到快取,當用戶付款成
功後再寫入資料庫。
基於redis快取,減少資料庫的訪問壓力,在秒殺之前就將資料庫的的資料放到快取中
秒殺開始後,使用者搶購商品訂單,下單成功後,減少庫存,此時我們是操作redis中秒殺商品庫存資料
那麼我們在什麼時候更新資料庫的庫存呢, ----------------->redis庫存為零,秒殺結束
使用redis快取,單執行緒伺服器,資料安全
定時任務spring-task實現:
定時任務主要常用的有兩種: spring - task 和quartz
我們在這介紹spring task :
它可以說是輕量級的quartz , 主要配置檔案和註解兩種形式
定時任務框架都是基於cron表示式完成定時時間指定。
往往都是6位字元
Seconds Minutes Hours DayofMonth Month DayofWeek
秒 分 時 月中某天 月 週中某天
每週一凌晨1點執行任務:0 0 1 ? * 2
每天15點55分執行任務:0 55 15 * * ?
每月6號凌晨執行任務:0 0 0 6 * ?
每隔10秒多久執行一次:0/10 0 0 * * ?
注意:月中某天和週中某天只能出現一個*,不能同時為*,如果兩者中有任意一個賦值,另一個往往都賦予?
把資料庫的商品快取到redis中 這樣就能,在下單的時候通過 通過redis總訪問資料
建立一個定時任務的工程 seckill_task
配置檔案:
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/applicationContext*.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
</web-app>
applicationContext-task .xml
<!--包掃描-->
<context:component-scan base-package="com.pinyougou.seckill.task"/>
<!--開啟註解驅動-->
<task:annotation-driven/>
seckillTask:主要是通過定時,從資料中獲得資料快取到redis資料庫中
@Component
public class SeckillTask {
@Autowired
private TbSeckillGoodsMapper seckillGoodsMapper;
@Autowired
private RedisTemplate redisTemplate;
@Scheduled(cron = "0/30 * * * * ?")//每30秒執行一次
public void synchronizeSeckillGoodsToRedis(){
//1.查詢需要秒殺的商品
//我們把符合的商品都查詢出來放到redis中
TbSeckillGoodsExample example = new TbSeckillGoodsExample();
TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1").
andStartTimeLessThanOrEqualTo(new Date()).
andEndTimeGreaterThanOrEqualTo(new Date()).
andStockCountEqualTo(0);
List<TbSeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example);
//2.將查詢的商品的資料放到redis中
//我們將查詢符合的資料放到hash格式放到redis中
for (TbSeckillGoods seckillGood : seckillGoods) {
redisTemplate.boundHashOps("SECKILL_GOODS").put(seckillGood.getId(),seckillGood);
//商品的詳情頁,我們可以通過商品的id取值,如下就是的
// List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
}
System.out.println("synchronizeSeckillGoodsToRedis worker finished...");
}
}
List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
這個資料我們在秒殺的詳情頁中展示,所以通過上面的格式儲存後,我們就可以通過商品的id取值
二.秒殺下單功能實現
構建秒殺的功能模組 web interface service
從redis 中獲取需要參加秒殺的商品
後臺程式碼:
service層:
@Service
@Transactional
public class SeckillServiceImpl implements SeckillService{
@Autowired
private RedisTemplate redisTemplate;
/**
* 從redis 中查詢所有要參加秒殺的商品
* @return
*/
@Override
public List<TbSeckillGoods> findAllSeckillGoodsFromRedis() {
//獲取redis中的資料
List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
return seckill_goods;
}
}
interface層和controller層:
/**
* 從redis中查詢所有的秒殺商品列表
*/
public List<TbSeckillGoods> findAllSeckillGoodsFromRedis();
//controller層:
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Reference
private SeckillService seckillService;
/**
* 從redis中查詢所有的要參加秒殺的商品
* @return
*/
@RequestMapping("/findSeckillList")
public List<TbSeckillGoods> findSeckillList(){
return seckillService.findAllSeckillGoodsFromRedis();
}
}
前臺頁面的實現:
我們通過$http內建物件傳送請求
//服務層
app.service('seckillService',function($http){
//查詢需要秒殺的商品列表
this.findSeckillList=function(){
return $http.get('seckill/findSeckillList.do');
}
});
//控制層
app.controller('seckillController' ,function($scope,$controller ,seckillService){
$controller('baseController',{$scope:$scope});//繼承
//,$location 跨域
//查詢需要從redis中獲得需要的秒殺商品資料
$scope.findSeckillList=function () {
seckillService.findSeckillList().success(function (response) {
$scope.seckillList=response;
})
}
});
我們通過秒殺頁面跳轉到秒殺的詳情頁,通過路由傳參
詳情頁面的展示:
思路:我們通過商品的id查詢商品的詳情,前臺頁面進行展示,注意倒計時的處理,我們通過angularjs中$interval 一個內建物件進行設定
後臺我們先從spring-sceurity中獲得使用者登入的資訊,然後從redis中獲得商品的資料,組裝訂單,然後在redis的商品庫中減去一,儲存訂單,當庫存為零的是時候我們,更新資料庫,清除redis中該商品的資料,注意:一定要在秒殺成功後,扣減庫存
/**
* 儲存秒殺的訂單
*/
@RequestMapping("/saveSeckillOrder")
public Result saveSeckillOrder(Long seckillGoodsId){
try {
//基於安全獲取登入人資訊
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
if(userId.equals("anonymousUser")){
return new Result(false,"請下登入,再下單");
}
seckillService.saveSeckillOrder(seckillGoodsId,userId);
return new Result(true,"秒殺下單成功");
} catch (RuntimeException e) {
e.printStackTrace();
return new Result(false,e.getMessage());
}catch (Exception e) {
e.printStackTrace();
return new Result(false,"秒殺下單失敗");
}
}
service層:
/**
* 儲存秒殺訂單
* @param seckillGoodsId
* @param userId
*/
@Override
public void saveSeckillOrder(Long seckillGoodsId, String userId) {
//從快取中獲取秒殺商品
TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);
if(seckillGoods==null || seckillGoods.getStockCount()<=0){
throw new RuntimeException("商品售完");
}
//組裝秒殺訂單資料
TbSeckillOrder seckillOrder = new TbSeckillOrder();
/*
tb_seckill_order
`id` bigint(20) NOT NULL COMMENT '主鍵',
`seckill_id` bigint(20) DEFAULT NULL COMMENT '秒殺商品ID',
`money` decimal(10,2) DEFAULT NULL COMMENT '支付金額',
`user_id` varchar(50) DEFAULT NULL COMMENT '使用者',
`seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
`create_time` datetime DEFAULT NULL COMMENT '建立時間',
`status` varchar(1) DEFAULT NULL COMMENT '狀態',
*/
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(seckillGoodsId);
seckillOrder.setMoney(seckillGoods.getCostPrice());
seckillOrder.setUserId(userId);
seckillOrder.setSellerId(seckillGoods.getSellerId());
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("1");//未支付
//設定秒殺商品庫存減一
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//儲存秒殺訂單
seckillOrderMapper.insert(seckillOrder);
if(seckillGoods.getStockCount()<=0){
//商品售完,沒有庫存後,需要更新資料庫中秒殺商品庫存資料
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
//清除redis中該商品
redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
}
//秒選下單成功後,扣減庫存
redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);
}
前臺程式碼:
//秒殺下單
this.saveSeckillOrder=function (seckillGoodsId) {
return $http.get('seckill/saveSeckillOrder.do?seckillGoodsId='+seckillGoodsId)
}
controller層:
//秒殺下單
$scope.saveSeckillOrder=function () {
seckillService.saveSeckillOrder($scope.seckillGoodsId).success(function (response) {
alert(response.message);
})
}
測試:儲存訂單成功,下面我們進行秒殺的優化
三.秒殺優化的解決方案
1.解決同一個人重複購買此商品的問題:
解決方案:使用者下單後,向redis中存放一個預支付資訊。當進入到下單的方法時,先判斷redis中是否存在該使用者的預支付資訊。
如果存在,則丟擲異常提醒先去支付已買商品。
service層:
2.解決超賣的問題
超賣是由於redis和mysql處理能力不同造成的。
因為使用者從redis中獲取商品資訊時,redis處理能力是很強勁的。而使用者搶到商品下訂單後,訂單儲存到mysql資料庫,資料庫本來
處理能力是沒有redis強勁的。所以,可能造成同時5個使用者,能夠為同一件商品下訂單,這是不允許的。
可以使用redis佇列解決。
使用list儲存形式。可以基於左右壓棧操作。
基於redis佇列,快取某個秒殺商品還剩多個庫存。
訊息佇列也可以實現。
在這我們通過redis的左壓棧實現
3.使用多執行緒解決操作mysql的問題
因為mysql執行效率比redis要低,所以,需要充分利用CPU的資源,提升mysql處理操作
可以基於spring整合多執行緒完成該操作。在spring配置檔案中配置執行緒池。
配置執行緒池:
我們將執行資料庫操作出去到執行緒中去
package com.pinyougou.seckill.service.impl;
import com.pinyougou.mapper.TbSeckillGoodsMapper;
import com.pinyougou.mapper.TbSeckillOrderMapper;
import com.pinyougou.pojo.TbSeckillGoods;
import com.pinyougou.pojo.TbSeckillOrder;
import com.pinyougou.util.IdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Date;
import java.util.Map;
public class CreateSeckillOrder implements Runnable {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IdWorker idWorker;
@Autowired
private TbSeckillOrderMapper seckillOrderMapper;
@Autowired
private TbSeckillGoodsMapper seckillGoodsMapper;
@Override
public void run() {
//從redis中取出我們需要的訂單任務
Map<String,Object> param = (Map<String, Object>) redisTemplate.boundListOps("seckill_order_queue").rightPop();
Long seckillGoodsId = (Long) param.get("seckillGoodsId");
String userId = (String) param.get("userId");
//從快取中獲取秒殺商品
TbSeckillGoods seckillGoods= (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);
//組裝秒殺訂單資料
TbSeckillOrder seckillOrder = new TbSeckillOrder();
/*
tb_seckill_order
`id` bigint(20) NOT NULL COMMENT '主鍵',
`seckill_id` bigint(20) DEFAULT NULL COMMENT '秒殺商品ID',
`money` decimal(10,2) DEFAULT NULL COMMENT '支付金額',
`user_id` varchar(50) DEFAULT NULL COMMENT '使用者',
`seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
`create_time` datetime DEFAULT NULL COMMENT '建立時間',
`status` varchar(1) DEFAULT NULL COMMENT '狀態',
*/
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(seckillGoodsId);
seckillOrder.setMoney(seckillGoods.getCostPrice());
seckillOrder.setUserId(userId);
seckillOrder.setSellerId(seckillGoods.getSellerId());
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("1");//未支付
//設定秒殺商品庫存減一
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
//儲存秒殺訂單
seckillOrderMapper.insert(seckillOrder);
//秒殺下單成功後,我們要儲存一份預支付的訂單到redis,中,下次我們再在redis中判斷,是否是第二次購買該商品
//解決同一個人,不能重複購買的問題
redisTemplate.boundSetOps("seckill_goods_"+seckillGoodsId).add(userId);
if(seckillGoods.getStockCount()<=0){
//商品售完,沒有庫存後,需要更新資料庫中秒殺商品庫存資料
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
//清除redis中該商品
redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
}
//秒選下單成功後,扣減庫存
redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);
}
}
在service中呼叫
注意注入我們配置的執行緒池:
4.排隊人數提醒
當一個請求進入下單的方法時,需要設定排隊人數加1,當排隊人多大於庫存一定值時(例如10,根據業務規則確定),丟擲異常,提醒排隊人數過多。
當一個人下單買完商品後,排隊人數減一(在多執行緒下單模組完成)