1.秒殺:
秒殺概念:所謂“秒殺”,就是網(wǎng)絡賣家發(fā)布一些超低價格的商品,所有買家在同一時間網(wǎng)上搶購的一種銷售方式。
通俗一點講就是網(wǎng)絡商家為促銷等目的組織的網(wǎng)上限時搶購活動。由于商品價格低廉,往往一上架就被搶購一空,有時只用一秒鐘。秒殺商品通常有兩種限制:庫存限制、時間限制。
需求:
1.2? 表結構說明
秒殺商品信息表
CREATETABLE`tb_seckill_goods`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`goods_id`bigint(20)DEFAULTNULLCOMMENT'spu ID',
`item_id`bigint(20)DEFAULTNULLCOMMENT'sku ID',
`title`varchar(100)DEFAULTNULLCOMMENT'標題',
`small_pic`varchar(150)DEFAULTNULLCOMMENT'商品圖片',
`price`decimal(10,2)DEFAULTNULLCOMMENT'原價格',
`cost_price`decimal(10,2)DEFAULTNULLCOMMENT'秒殺價格',
`seller_id`varchar(100)DEFAULTNULLCOMMENT'商家ID',
`create_time`datetimeDEFAULTNULLCOMMENT'添加日期',
`check_time`datetimeDEFAULTNULLCOMMENT'審核日期',
`status`char(1)DEFAULTNULLCOMMENT'審核狀態(tài),0未審核,1審核通過,2審核不通過',
`start_time`datetimeDEFAULTNULLCOMMENT'開始時間',
`end_time`datetimeDEFAULTNULLCOMMENT'結束時間',
`num`int(11)DEFAULTNULLCOMMENT'秒殺商品數(shù)',
`stock_count`int(11)DEFAULTNULLCOMMENT'剩余庫存數(shù)',
`introduction`varchar(2000)DEFAULTNULLCOMMENT'描述',
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8
(1)秒殺頻道首頁列出秒殺商品(4)點擊立即搶購實現(xiàn)秒殺下單,下單時扣減庫存。當庫存為0或不在活動期范圍內(nèi)時無法秒殺。(5)秒殺下單成功,直接跳轉到支付頁面(微信掃碼),支付成功,跳轉到成功頁,填寫收貨地址、電話、收件人等信息,完成訂單。(6)當用戶秒殺下單5分鐘內(nèi)未支付,取消預訂單,調(diào)用微信支付的關閉訂單接口,恢復庫存。
秒殺商品訂單表
CREATETABLE`tb_seckill_order`(
`id`bigint(20)NOTNULLCOMMENT'主鍵',
`seckill_id`bigint(20)DEFAULTNULLCOMMENT'秒殺商品ID',
`money`decimal(10,2)DEFAULTNULLCOMMENT'支付金額',
`user_id`varchar(50)DEFAULTNULLCOMMENT'用戶',
`seller_id`varchar(50)DEFAULTNULLCOMMENT'商家',
`create_time`datetimeDEFAULTNULLCOMMENT'創(chuàng)建時間',
`pay_time`datetimeDEFAULTNULLCOMMENT'支付時間',
`status`char(1)DEFAULTNULLCOMMENT'狀態(tài),0未支付,1已支付',
`receiver_address`varchar(200)DEFAULTNULLCOMMENT'收貨人地址',
`receiver_mobile`varchar(20)DEFAULTNULLCOMMENT'收貨人電話',
`receiver`varchar(20)DEFAULTNULLCOMMENT'收貨人',
`transaction_id`varchar(30)DEFAULTNULLCOMMENT'交易流水',
PRIMARYKEY(`id`)
)ENGINE=InnoDBDEFAULTCHARSET=utf8;
需要解決的問題
如何解決分布式事務實現(xiàn),分布式事務鎖實現(xiàn),商品超賣等問題
秒殺服務端:
2.秒殺商品存入redis緩存:

在秒殺服務中將秒殺商品存入mysql中,設置定時任務將秒殺商品從mysql中查詢出來存入緩存中,以redis以hash類型進行數(shù)據(jù)存儲,用戶可以在前端看到展示的秒殺商品
秒殺服務搭建
1.changgou_service_seckill模塊創(chuàng)建
1.啟動類創(chuàng)建
1.1
redisTemplate序列化
//設置redisTemplate序列化
?
publicRedisTemplate<Object,Object>redisTemplate(RedisConnectionFactoryredisConnectionFactory){
//創(chuàng)建redis模板
RedisTemplate<Object,Object>template=newRedisTemplate<>();
//關聯(lián)redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
//創(chuàng)建 序列化類
GenericToStringSerializergenericToStringSerializer=newGenericToStringSerializer(Object.class);
//序列化類 對象映射設置
//設置value轉化格式和Key的轉化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(newStringRedisSerializer());
template.afterPropertiesSet();
returntemplate;
}
2.鑒權公鑰文件
config 配置類
packagecom.changgou.seckill.config;
?
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.core.io.ClassPathResource;
importorg.springframework.core.io.Resource;
importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
importorg.springframework.security.config.annotation.web.builders.HttpSecurity;
importorg.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
importorg.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
importorg.springframework.security.oauth2.provider.token.TokenStore;
importorg.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
importorg.springframework.security.oauth2.provider.token.store.JwtTokenStore;
?
importjava.io.BufferedReader;
importjava.io.IOException;
importjava.io.InputStreamReader;
importjava.util.stream.Collectors;
?
@Configuration
@EnableResourceServer
//開啟方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{
?
//公鑰
privatestaticfinalStringPUBLIC_KEY="public.key";
?
/***
* 定義JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
publicTokenStoretokenStore(JwtAccessTokenConverterjwtAccessTokenConverter) {
returnnewJwtTokenStore(jwtAccessTokenConverter);
?? }
?
/***
* 定義JJwtAccessTokenConverter
* @return
*/
@Bean
publicJwtAccessTokenConverterjwtAccessTokenConverter() {
JwtAccessTokenConverterconverter=newJwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
returnconverter;
?? }
/**
* 獲取非對稱加密公鑰 Key
* @return 公鑰 Key
*/
privateStringgetPubKey() {
Resourceresource=newClassPathResource(PUBLIC_KEY);
try{
InputStreamReaderinputStreamReader=newInputStreamReader(resource.getInputStream());
BufferedReaderbr=newBufferedReader(inputStreamReader);
returnbr.lines().collect(Collectors.joining("\n"));
}catch(IOExceptionioe) {
returnnull;
? ? ?? }
?? }
?
/***
* Http安全配置,對每個到達系統(tǒng)的http請求鏈接進行校驗
* @param http
* @throws Exception
*/
@Override
publicvoidconfigure(HttpSecurityhttp)throwsException{
//所有請求必須認證通過
http.authorizeRequests()
.anyRequest()
.authenticated();//其他地址需要認證授權
?? }
}
3.通過網(wǎng)關訪問
//所有需要傳遞令牌的地址
publicstaticStringfilterPath="/api/worder/**,/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
packagecom.changgou.web.gateway.filter;
?
publicclassUrlFilter{
?
//所有需要傳遞令牌的地址
publicstaticStringfilterPath="/api/worder/**,/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
?
publicstaticbooleanhasAuthorize(Stringurl){
?
String[]split=filterPath.replace("**","").split(",");
?
for(Stringvalue:split) {
?
if(url.startsWith(value)){
returntrue;//代表當前的訪問地址是需要傳遞令牌的
? ? ? ? ?? }
? ? ?? }
?
returnfalse;//代表當前的訪問地址是不需要傳遞令牌的
?? }
}
3.1更改網(wǎng)關配置文件,添加請求路由轉發(fā)
#秒殺微服務
?? - id: changgou_seckill_route
? ?? uri: lb://seckill
? ?? predicates:
? ? ?? - Path=/api/seckill/**
? ?? filters:
? ? ?? - StripPrefix=1
changgou_service下
模塊創(chuàng)建流程:
創(chuàng)建changgou_service_seckill
pom引入
common——db依賴? eureka依賴 changgou_service_order_api依賴 changgou_service_seckill_api依賴
changgou_service_goods_api依賴
spring-rabbit依賴
changgou_service_order_api模塊創(chuàng)建
數(shù)據(jù)庫表實體創(chuàng)建 (pojo)秒殺商品 秒殺訂單
feign包
4.秒殺時間段計算:
package com.changgou.util;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Calendar;import java.util.Date;import java.util.List;
public class DateUtil {
從格式轉成格式獲取指定日期的凌晨
/***
* 時間增加N分鐘
* @param date
* @param minutes
* @return
*/
publicstaticDateaddDateMinutes(Datedate,intminutes){
Calendarcalendar=Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE,minutes);// 24小時制
date=calendar.getTime();
returndate;
}
?
/***
* 時間遞增N小時
* @param hour
* @return
*/
publicstaticDateaddDateHour(Datedate,inthour){
Calendarcalendar=Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR,hour);// 24小時制
date=calendar.getTime();
returndate;
}
?
/***
* 獲取時間菜單
* @return
*/
publicstaticList<Date>getDateMenus(){
//定義一個List<Date>集合,存儲所有時間段
List<Date>dates=newArrayList<Date>();
//循環(huán)12次
Datedate=toDayStartHour(newDate());//凌晨
for(inti=0;i<12;i++) {
//每次遞增2小時,將每次遞增的時間存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
?? }
?
//判斷當前時間屬于哪個時間范圍
Datenow=newDate();
for(Datecdate:dates) {
//開始時間<=當前時間<開始時間+2小時
if(cdate.getTime()<=now.getTime()&&now.getTime()<addDateHour(cdate,2).getTime()){
now=cdate;
break;
? ? ?? }
?? }
?
//當前需要顯示的時間菜單
List<Date>dateMenus=newArrayList<Date>();
for(inti=0;i<5;i++) {
dateMenus.add(addDateHour(now,i*2));
?? }
returndateMenus;
}
?
/***
* 時間轉成yyyyMMddHH
* @param date
* @return
*/
publicstaticStringdate2Str(Datedate){
SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyyMMddHH");
returnsimpleDateFormat.format(date);
}
}

每個秒殺時間段間隔兩小時,一天存在12個秒殺時間段,每個秒殺時間段有商品,每個秒殺商品存在開始時間結束時間(只要每個秒殺商品時間大于開始時間段,并且小于秒殺商品結束時間段,那么該秒殺商品就是屬于這個秒殺時間段的)所以每個秒殺時間段中有哪些商品呢?上面提供了秒殺Utills
工具類中測試方法獲取12個時間段:
publicstaticvoidmain(String[]args) {
//集合存儲數(shù)據(jù)結果
List<Date>dateList=newArrayList<>();
?
//獲取本日凌晨時間點
DatestartHour=DateUtil.toDayStartHour(newDate());
//循環(huán)12次
for(inti=0;i<12;i++) {
dateList.add(addDateHour(startHour,i*2));
?
?
?? }
for(Datedate:dateList) {
//輸出打印 日期格式化
SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Stringformat=simpleDateFormat.format(date);
System.out.println(format);
?? }
?
}
需求:靜態(tài)原型中只展示5個時間段
/***
* 獲取時間菜單
* @return
*/
publicstaticList<Date>getDateMenus(){
//定義一個List<Date>集合,存儲所有時間段
List<Date>dates=newArrayList<Date>();
//循環(huán)12次
Datedate=toDayStartHour(newDate());//凌晨
for(inti=0;i<12;i++) {
//每次遞增2小時,將每次遞增的時間存入到List<Date>集合中
dates.add(addDateHour(date,i*2));
?? }
?
//判斷當前時間屬于哪個時間范圍
Datenow=newDate();
for(Datecdate:dates) {
//開始時間<=當前時間<開始時間+2小時
if(cdate.getTime()<=now.getTime()&&now.getTime()<addDateHour(cdate,2).getTime()){
now=cdate;
break;
? ? ?? }
?? }
?
//當前需要顯示的時間菜單
List<Date>dateMenus=newArrayList<Date>();
for(inti=0;i<5;i++) {
dateMenus.add(addDateHour(now,i*2));
?? }
returndateMenus;
}
5.秒殺商品存入緩存實現(xiàn):
1.定義定時任務,查詢符合條件的秒殺商品
邏輯:
1.獲取時間段集合并循環(huán)遍歷出每一個時間段
2.獲取每個時間段名稱,用于后續(xù)redis中Key的設置
3.秒殺商品狀態(tài)必須為審核通過status=1
4.商品庫存?zhèn)€數(shù)>0
5.商品秒殺開始時間>=當前時間段
6.秒殺商品結束<當前時間段+2
7.排除之前已經(jīng)加載到Redis緩存中的商品數(shù)據(jù)
8.執(zhí)行查詢獲取對應的結果集
2.將秒殺商品存入緩存
3.定義定時任務類 SecKillGoodsPushTask
1.秒殺服務啟動類添加定時任務注解
2.定時任務包task 方法 方法注解Scheduled(設置定時執(zhí)行時間):
publice void loadSecKillGoodsToRedis(){
1.創(chuàng)建秒殺時間段展示集合
2.遍歷進行格式轉化(使用工具類)
3.獲取每個時間段名稱作為redis的Key
4.進行查詢 秒殺商品查詢mapper進行注入
秒殺Mapper.selectByExample(examle);創(chuàng)建example傳入操作的秒殺商品表實體類,獲取查詢條件對象,設置status狀態(tài),addGreaterThan(屬性名 大于的值)商品庫存?zhèn)€數(shù)>0,addGreaterThanOrEqualTo(開始時間屬性名稱,開始時間值ps:注意格式化)設置商品秒殺開始時間>=當前時間段,addLessThan(結束時間屬性名稱,)秒殺商品結束<當前時間段+2,排除之前已經(jīng)加載到Redis緩存中的商品數(shù)據(jù) 注入RedisTemplate? 定義常量 調(diào)用redisTemplate.boundHashOps(常量+redisKey).keys();獲取redis中的值,拿到值進行判斷有沒有這個值(keys!=null&&keys.size()>0),執(zhí)行查詢獲取對應的結果集,遍歷結果集redisTemplate.opsForHash().put(大Key,秒殺商品Id小key,秒殺商品對象)
注意:大Key是boundHashOps中常量+之前獲取的redisKey,小Key為秒殺商品實體中的Id,value為秒殺商品對象,這三個值可以通過opsForHash傳入進行秒殺商品存入緩存的添加
}
6.秒殺商品列表展示:
需求:當前已經(jīng)完成了秒殺時間段菜單的顯示,當用戶在切換時間段時按照不同的時間段展示不同時間段下的秒殺商品
實現(xiàn)流程:秒殺渲染服務基于Feign會調(diào)用秒殺服務,在秒殺服務定義方法
秒殺服務定義service接口 SeckillGoodsService?
接口方法:List<SeckillGoods> list(String time)
實現(xiàn)類:SecKillGoodsServiceImpl
返回值時間集合list
表現(xiàn)層:SecKillGoodsController
返回值:查詢商品列表集合給前端:seckillGoodsList
返回值:Result<List<SecKillGoods>>
Oauth2必須對所有請求進行放行,在配置類ResourceSeviceConfig? configura方法中對請求路徑進行放行:

再定義Feign接口進行Feign暴露 :

7.秒殺商品列表秒殺渲染服務顯示數(shù)據(jù)
實現(xiàn)流程:在秒殺渲染服務controller中注入SecKillGoodsFeign,進行遠程調(diào)用
SecKillGoodsController中定義方法
/**
* 秒殺時間段下商品列表顯示
*/
@Autowired
privateSecKillGoodsFeignsecKillGoodsFeign;
?
?
@RequestMapping("/list")
@ResponseBody
publicResult<List<SeckillGoods>>list(Stringtime){
Result<List<SeckillGoods>>secKillGoodsList=secKillGoodsFeign.list(time);
?
returnsecKillGoodsList;
}
前端代碼發(fā)起異步,經(jīng)過前端網(wǎng)關通過類路徑調(diào)用/api/wseckillgoods/list?time 傳入時間參數(shù)獲取 后臺渲染服務controller中的返回值,前端還需要通過用戶點擊秒殺時間獲取時間參數(shù)傳入后臺渲染服務controller,這樣就完成了秒殺時間段下商品列表的數(shù)據(jù)展示了


測試:


bug:沒查到?因為時間格式不對 使用時間格式化工具:
DateUtil.formatStr(time)

時間參數(shù)正確 展示成功

立即搶購實現(xiàn)秒殺下單Js