一、秒殺(高并發)系統關注的問題
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwIjNx8CX39CXy8CXycXZpZVZnFWbp9zZuBnLkV3ZtdXc5hndh9CXwQzNhJGNyYTN2kTNzkTMtUGall3LcdXYy9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
秒殺業務:
秒殺具有瞬間高并發的特點,針對這一特點,必須要做限流 + 異步 + 緩存(頁面靜态化)+ 獨立部署
限流方式:
1. 前端限流,一些高并發的網站直接在前端頁面開始限流,例如:小米的驗證碼設計
複制
- nginx,限流,直接負載部分請求到錯誤的靜态頁面:令牌算法漏鬥算法
- 網關限流,限流的過濾器
- 代碼中使用分布式信号量
- rabbitmq 限流(能者多勞: chanel.basicOos(1)),保證發揮所有伺服器的性能。
秒殺流程:
1、先新增秒殺場次到 DB【背景系統新增】
2、再關聯商品【背景系統關聯】
3、定時任務将最近三天的場次+關聯商品上傳到 redis 中【定時 上架 3 天内的秒殺場次+商品】
二、建立秒殺服務
添加 gateway 路由轉發
- id: coupon_route
uri: lb://gulimall-coupon
predicates:
- Path=/api/coupon/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
複制
登入背景管理界面,添加秒殺場次
**例如: **添加 8 點場,對應表 sms_seckill_session【秒殺場次表】
秒殺場次關聯商品
sms_seckill_sku_relation【關聯表】
字段:
promotion_id【活動 id】、promotion_session_id【活動場次 id】、sku_id、排序、價格、總量、每人限購數量
- SeckillSkuRelationServiceImpl.java
public PageUtils queryPage(Map<String, Object> params) {
QueryWrapper<SeckillSkuRelationEntity> queryWrapper = new QueryWrapper<>();
String promotionSessionId = (String) params.get("promotionSessionId");
if (!StringUtils.isEmpty(promotionSessionId)) {
queryWrapper.eq("promotion_session_id", promotionSessionId);
}
IPage<SeckillSkuRelationEntity> page = this.page(
new Query<SeckillSkuRelationEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
複制
建立秒殺 gulimall-seckill 微服務
- redis、openFeign、spring boot devtools、spring web、lombok
<dependencies>
<dependency>
<groupId>com.oy.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
複制
三、定時任務-QUARTZ
多種實作方式:
Timer、線程池、mq 的延遲隊列、QUARTZ【搭配 cron 表達式使用】、spring 架構的定時任務,可以整合 QUARTZ(springboot 預設定時任務架構不是 QUARTZ,如果需要使用引入即可)
最終解決方案:使用異步任務 + 定時任務來完成定時任務不阻塞的功能
1、減輕 DB 壓力,定時任務查詢需要上架的秒殺商品上架到 redis 中,庫存資訊等
2、文法:秒 分 時 日 月 周 年 (spring 不支援年,是以可以不寫)
http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
Format
A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:
Field Name Mandatory Allowed Values Allowed Special Characters
Seconds YES 0-59 , - * /
Minutes YES 0-59 , - * /
Hours YES 0-23 , - * /
Day of month YES 1-31 , - * ? / L W
Month YES 1-12 or JAN-DEC , - * /
Day of week YES 1-7 or SUN-SAT , - * ? / L #
Year NO empty, 1970-2099 , - * /
特殊字元:
,:枚舉;
(cron="7,9,23****?"):任意時刻的7,9,23秒啟動這個任務;
-:範圍:
(cron="7-20****?""):任意時刻的7-20秒之間,每秒啟動一次
*:任意;
指定位置的任意時刻都可以
/:步長;
(cron="7/5****?"):第7秒啟動,每5秒一次;
(cron="*/5****?"):任意秒啟動,每5秒一次;
? :(出現在日和周幾的位置):為了防止日和周沖突,在周和日上如果要寫通配符使用?
(cron="***1*?"):每月的1号,而且必須是周二然後啟動這個任務;
L:(出現在日和周的位置)”,
last:最後一個
(cron="***?*3L"):每月的最後一個周二
W:Work Day:工作日
(cron="***W*?"):每個月的工作日觸發
(cron="***LW*?"):每個月的最後一個工作日觸發
#:第幾個
(cron="***?*5#2"):每個月的 第2個周4
複制
3、線上定時器 https://cron.qqe2.com/
4、Example
Examples
Here are some full examples:
**Expression** **Meaning**
0 0 12 * * ? Fire at 12pm (noon) every day
0 15 10 ? * * Fire at 10:15am every day
0 15 10 * * ? Fire at 10:15am every day
0 15 10 * * ? * Fire at 10:15am every day
0 15 10 * * ? 2005 Fire at 10:15am every day during the year 2005
0 * 14 * * ? Fire every minute starting at 2pm and ending at 2:59pm, every day
0 0/5 14 * * ? Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day
0 0/5 14,18 * * ? Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5 minutes starting at 6pm and ending at 6:55pm, every day
0 0-5 14 * * ? Fire every minute starting at 2pm and ending at 2:05pm, every day
0 10,44 14 ? 3 WED Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.
0 15 10 ? * MON-FRI Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday
0 15 10 15 * ? Fire at 10:15am on the 15th day of every month
0 15 10 L * ? Fire at 10:15am on the last day of every month
0 15 10 L-2 * ? Fire at 10:15am on the 2nd-to-last last day of every month
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005 Fire at 10:15am on every last friday of every month during the years 2002, 2003, 2004 and 2005
0 15 10 ? * 6#3 Fire at 10:15am on the third Friday of every month
0 0 12 1/5 * ? Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
0 11 11 11 11 ? Fire every November 11th at 11:11am.
複制
springboot 開啟定時任務 Demo
解決:使用異步任務 + 定時任務來完成定時任務不阻塞的功能
1、加在類上
@Component
@EnableScheduling開啟定時任務【spring 預設是使用自己的定時任務】
@EnableAsync:開啟異步任務【定時任務不應該阻塞,需要異步執行(不加該注解是同步的,例如方法内部sleep會阻塞)】
解決辦法:1、自己異步執行【CompletableFuture.runAsync】
2、使用spring的 定時任務線程池scheduling.pool.size: 5
3、使用springboot的異步定時任務@EnableAsync
然後配置異步任務的屬性
spring:
task:
execution:
pool:
core-size: 5
max-size: 50
然後給定時任務方法加上@Async【這個注解就是異步執行,不一定是定時任務】
複制
2、加載異步定時任務方法上
@Async 異步執行的方法标注
@Scheduled(cron = "*/5 * * ? * 4")
複制
3、編寫任務
每周4的任意秒啟動,5S一次執行
複制
4、spring 注意
1)spring周一都周天就是1-7
2)沒有年,隻有6個
/**
* 定時任務
* 1、@EnableScheduling 開啟定時任務
* 2、@Scheduled開啟一個定時任務
* 3、自動配置類TaskSchedulingAutoConfiguration
* 異步任務
* 1、@EnableAsync:開啟異步任務
* 2、@Async:給希望異步執行的方法标注
* 3、自動配置類TaskExecutionAutoConfiguration
*/
@Slf4j
@Component
// @EnableAsync
// @EnableScheduling
public class HelloScheduled {
/**
* 1、在Spring中表達式是6位組成,不允許第七位的年份
* 2、在周幾的的位置,1-7代表周一到周日
* 3、定時任務不該阻塞。預設是阻塞的
* 1)、可以讓業務以異步的方式,自己送出到線程池
* CompletableFuture.runAsync(() -> {
* },execute);
*
* 2)、支援定時任務線程池;設定 TaskSchedulingProperties
* spring.task.scheduling.pool.size: 5
*
* 3)、讓定時任務異步執行
* 異步任務
*
* 解決:使用異步任務 + 定時任務來完成定時任務不阻塞的功能
*
*/
@Async
@Scheduled(cron = "*/5 * * ? * 4")
public void hello() {
log.info("hello...");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
複制
四、秒殺架構設計
4.1 秒殺架構圖
- 項目獨立部署,獨立秒殺子產品
gulimall-seckill
- 使用定時任務每天三點上架最新秒殺商品,削減高峰期壓力
- 秒殺連結加密,為秒殺商品添加唯一商品随機碼,在開始秒殺時才暴露接口
- 庫存預熱,先從資料庫中扣除一部分庫存以
的形式存儲在 redis 中redisson 信号量
- 隊列削峰,秒殺成功後立即傳回,然後以發送消息的形式建立訂單
4.2 存儲模型設計
- 秒殺場次存儲的
可以當做List
在hash key
中獲得對應的商品資料SECKILL_CHARE_PREFIX
//存儲的秒殺場次對應資料
//K: SESSION_CACHE_PREFIX + startTime + "_" + endTime
//V: sessionId+"-"+skuId的List
private final String SESSION_CACHE_PREFIX = "seckill:sessions:";
//存儲的秒殺商品資料
//K: 固定值SECKILL_CHARE_PREFIX
//V: hash,k為sessionId+"-"+skuId,v為對應的商品資訊SeckillSkuRedisTo
private final String SECKILL_CHARE_PREFIX = "seckill:skus";
//K: SKU_STOCK_SEMAPHORE+商品随機碼
//V: 秒殺的庫存件數
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随機碼
複制
- 存儲後的效果
- 用來存儲的 to
@Data
public class SeckillSkuRedisTo {
private Long id;
/**
* 活動id
*/
private Long promotionId;
/**
* 活動場次id
*/
private Long promotionSessionId;
/**
* 商品id
*/
private Long skuId;
/**
* 秒殺價格
*/
private BigDecimal seckillPrice;
/**
* 秒殺總量
*/
private Integer seckillCount;
/**
* 每人限購數量
*/
private Integer seckillLimit;
/**
* 排序
*/
private Integer seckillSort;
//以上都為SeckillSkuRelationEntity的屬性
//skuInfo
private SkuInfoVo skuInfoVo;
//目前商品秒殺的開始時間
private Long startTime;
//目前商品秒殺的結束時間
private Long endTime;
//目前商品秒殺的随機碼
private String randomCode;
}
複制
4.3 商品上架
4.3.1 定時上架
- 開啟對定時任務的支援
@EnableAsync //開啟對異步的支援,防止定時任務之間互相阻塞
@EnableScheduling //開啟對定時任務的支援
@Configuration
public class ScheduledConfig {
}
複制
- 每天淩晨三點遠端調用
服務上架最近三天的秒殺商品coupon
- 由于在分布式情況下該方法可能同時被調用多次,是以加入分布式鎖,同時隻有一個服務可以調用該方法
//秒殺商品上架功能的鎖
private final String upload_lock = "seckill:upload:lock";
/**
* 定時任務
* 每天三點上架最近三天的秒殺商品
*/
@Async
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days() {
//為避免分布式情況下多服務同時上架的情況,使用分布式鎖
RLock lock = redissonClient.getLock(upload_lock);
try {
lock.lock(10, TimeUnit.SECONDS);
secKillService.uploadSeckillSkuLatest3Days();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
@Override
public void uploadSeckillSkuLatest3Days() {
R r = couponFeignService.getSeckillSessionsIn3Days();
if (r.getCode() == 0) {
List<SeckillSessionWithSkusVo> sessions = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
});
//在redis中分别儲存秒殺場次資訊和場次對應的秒殺商品資訊
saveSecKillSession(sessions);
saveSecKillSku(sessions);
}
}
複制
4.3.2 擷取最近三天的秒殺資訊
- 擷取最近三天的秒殺場次資訊,再通過秒殺場次 id 查詢對應的商品資訊
@Override
public List<SeckillSessionEntity> getSeckillSessionsIn3Days() {
QueryWrapper<SeckillSessionEntity> queryWrapper = new QueryWrapper<SeckillSessionEntity>()
.between("start_time", getStartTime(), getEndTime());
List<SeckillSessionEntity> seckillSessionEntities = this.list(queryWrapper);
List<SeckillSessionEntity> list = seckillSessionEntities.stream().map(session -> {
List<SeckillSkuRelationEntity> skuRelationEntities = seckillSkuRelationService.list(new QueryWrapper<SeckillSkuRelationEntity>().eq("promotion_session_id", session.getId()));
session.setRelations(skuRelationEntities);
return session;
}).collect(Collectors.toList());
return list;
}
//目前天數的 00:00:00
private String getStartTime() {
LocalDate now = LocalDate.now();
LocalDateTime time = now.atTime(LocalTime.MIN);
String format = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
//目前天數+2 23:59:59..
private String getEndTime() {
LocalDate now = LocalDate.now();
LocalDateTime time = now.plusDays(2).atTime(LocalTime.MAX);
String format = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return format;
}
複制
4.3.3 在 redis 中儲存秒殺場次資訊
private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
if(sessions != null && sessions.size() > 0){
sessions.stream().forEach(session -> {
//擷取目前活動的開始和結束時間的時間戳
long startTime = session.getStartTime().getTime();
long endTime = session.getEndTime().getTime();
//存入到Redis中的key
String key = SESSION_CACHE_PREFIX + startTime + "_" + endTime;
//判斷Redis中是否有該資訊,如果沒有才進行添加
Boolean hasKey = redisTemplate.hasKey(key);
//緩存活動資訊
if (!hasKey) {
//擷取到活動中所有商品的skuId
List<String> skuIds = session.getRelationSkus().stream()
.map(item -> item.getPromotionSessionId() + "-" + item.getSkuId().toString()).collect(Collectors.toList());
redisTemplate.opsForList().leftPushAll(key,skuIds);
}
});
}
}
複制
4.3.4 在 redis 中儲存秒殺商品資訊
private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) {
if(sessions != null && sessions.size() > 0){
sessions.stream().forEach(session -> {
//準備hash操作,綁定hash
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
session.getRelationSkus().stream().forEach(seckillSkuVo -> {
//生成随機碼
String token = UUID.randomUUID().toString().replace("-", "");
String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
if (!operations.hasKey(redisKey)) {
//緩存我們商品資訊
SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo();
Long skuId = seckillSkuVo.getSkuId();
//1、先查詢sku的基本資訊,調用遠端服務
R info = productFeignService.info(skuId);
if (info.getCode() == 0) {
SkuInfoVo skuInfo = info.getData("skuInfo",new TypeReference<SkuInfoVo>(){});
redisTo.setSkuInfoVo(skuInfo);
}
//2、sku的秒殺資訊
BeanUtils.copyProperties(seckillSkuVo,redisTo);
//3、設定目前商品的秒殺時間資訊
redisTo.setStartTime(session.getStartTime().getTime());
redisTo.setEndTime(session.getEndTime().getTime());
//4、設定商品的随機碼(防止惡意攻擊)
redisTo.setRandomCode(token);
//序列化json格式存入Redis中
String seckillValue = JSON.toJSONString(redisTo);
operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue);
//如果目前這個場次的商品庫存資訊已經上架就不需要上架
//5、使用庫存作為分布式Redisson信号量(限流)
// 使用庫存作為分布式信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
// 商品可以秒殺的數量作為信号量
semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
}
});
});
}
}
複制
4.4 首頁展示秒殺活動
-
業務說明和邏輯分析
業務說明:展示符合目前頁面時間的秒殺活動,把關聯的商品都顯示出來
邏輯分析:
-
判斷目前時間是否落在了活動資訊 startend 之間
long time = new Date().getTime(); 然後判斷這個 time 在哪個活動的 start_end 之間,因為 start_end 也是 long 類型,與 1970 的內插補點
查詢所有場次的 key 資訊:keys seckill:sessions: 【比對所有】
java 代碼:redisTemplate.keys(“seckill:sessions:_“),然後周遊 key 獲得 start、end
- 傳回商品資訊的時候,要屏蔽掉随機碼資訊【這個業務還是需要的,在商品頁,如果目前商品參與了秒殺,不傳回随機碼資訊】
-
@Controller
public class SeckillController {
@Autowired
private SeckillService seckillService;
@GetMapping(value = "/getCurrentSeckillSkus")
@ResponseBody
public R getCurrentSeckillSkus(){
// 擷取到目前可以參加秒殺商品的資訊
List<SeckillSkuRedisTo> vos = seckillService.getCurrentSeckillSkus();
return R.ok().setData(vos);
}
}
複制
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
Set<String> keys = redisTemplate.keys(SESSION_CACHE_PREFIX + "*");
if(keys != null && keys.size() > 0){
long currentTime = System.currentTimeMillis();
for (String key : keys) {
String replace = key.replace(SESSION_CACHE_PREFIX, "");
String[] split = replace.split("_");
long startTime = Long.parseLong(split[0]);
long endTime = Long.parseLong(split[1]);
// 目前秒殺活動處于有效期内
if(currentTime > startTime && currentTime < endTime){
// 取出目前秒殺活動對應商品存儲的hash key
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
// 取出存儲的商品資訊并傳回
List<SeckillSkuRedisTo> collect = range.stream().map(s -> {
String json = ops.get(s);
SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
return redisTo;
}).collect(Collectors.toList());
return collect;
}
}
}
return null;
}
複制
首頁擷取并拼裝資料
<div class="swiper-slide">
<!-- 動态拼裝秒殺商品資訊 -->
<ul id="seckillSkuContent"></ul>
</div>
<script type="text/javascript">
$.get("http://seckill.gulimall.com/getCurrentSeckillSkus", function (res) {
if (res.data.length > 0) {
res.data.forEach(function (item) {
$("<li onclick='toDetail(" + item.skuId + ")'></li>").append($("<img style='width: 130px; height: 130px' src='" + item.skuInfoVo.skuDefaultImg + "' />"))
.append($("<p>"+item.skuInfoVo.skuTitle+"</p>"))
.append($("<span>" + item.seckillPrice + "</span>"))
.append($("<s>" + item.skuInfoVo.price + "</s>"))
.appendTo("#seckillSkuContent");
})
}
})
function toDetail(skuId) {
location.href = "http://item.gulimall.com/" + skuId + ".html";
}
</script>
複制
首頁展示效果
4.5 擷取目前商品的秒殺資訊
@ResponseBody
@GetMapping(value = "/getSeckillSkuInfo/{skuId}")
public R getSeckillSkuInfo(@PathVariable("skuId") Long skuId) {
SeckillSkuRedisTo to = secKillService.getSeckillSkuInfo(skuId);
return R.ok().setData(to);
}
@Override
public SeckillSkuRedisTo getSeckillSkuInfo(Long skuId) {
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
//擷取所有商品的hash key
Set<String> keys = ops.keys();
for (String key : keys) {
//通過正規表達式比對 數字-目前skuid的商品
if (Pattern.matches("\\d-" + skuId,key)) {
String v = ops.get(key);
SeckillSkuRedisTo redisTo = JSON.parseObject(v, SeckillSkuRedisTo.class);
//目前商品參與秒殺活動
if (redisTo!=null){
long current = System.currentTimeMillis();
//目前活動在有效期,暴露商品随機碼傳回
if (redisTo.getStartTime() < current && redisTo.getEndTime() > current) {
return redisTo;
}
//目前商品不再秒殺有效期,則隐藏秒殺所需的商品随機碼
redisTo.setRandomCode(null);
return redisTo;
}
}
}
return null;
}
複制
在查詢商品詳情頁的接口中查詢秒殺對應資訊
更改商品詳情頁的顯示效果
<li style="color: red" th:if="${item.seckillSkuVo != null}">
<span th:if="${#dates.createNow().getTime() < item.seckillSkuVo.startTime}">
商品将會在[[${#dates.format(new
java.util.Date(item.seckillSkuVo.startTime),"yyyy-MM-dd
HH:mm:ss")}]]進行秒殺
</span>
<span
th:if="${#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime}"
>
秒殺價 [[${#numbers.formatDecimal(item.seckillSkuVo.seckillPrice,1,2)}]]
</span>
</li>
複制
<div class="box-btns-two" th:if="${item.seckillSkuVo == null }">
<a
class="addToCart"
href="http://cart.gulimall.com/addToCart"
th:attr="skuId=${item.info.skuId}"
>
加入購物車
</a>
</div>
<div
class="box-btns-two"
th:if="${item.seckillSkuVo != null && (#dates.createNow().getTime() >= item.seckillSkuVo.startTime && #dates.createNow().getTime() <= item.seckillSkuVo.endTime)}"
>
<a
class="seckill"
href="#"
th:attr="skuId=${item.info.skuId},sessionId=${item.seckillSkuVo.promotionSessionId},code=${item.seckillSkuVo.randomCode}"
>
立即搶購
</a>
</div>
複制
五、秒殺
5.1 秒殺接口
- 點選立即搶購,會發送請求
- 秒殺請求會對請求校驗
,通過校驗的則秒殺成功,發送消息建立訂單時效、商品随機碼、目前使用者是否已經搶購過目前商品、庫存和購買量
@GetMapping("/kill")
public String kill(@RequestParam("killId") String killId,
@RequestParam("key")String key,
@RequestParam("num")Integer num,
Model model) {
String orderSn= null;
try {
orderSn = secKillService.kill(killId, key, num);
model.addAttribute("orderSn", orderSn);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
}
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
String json = ops.get(killId);
String orderSn = null;
if (!StringUtils.isEmpty(json)){
SeckillSkuRedisTo redisTo = JSON.parseObject(json, SeckillSkuRedisTo.class);
//1. 驗證時效
long current = System.currentTimeMillis();
if (current >= redisTo.getStartTime() && current <= redisTo.getEndTime()) {
//2. 驗證商品和商品随機碼是否對應
String redisKey = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
if (redisKey.equals(killId) && redisTo.getRandomCode().equals(key)) {
//3. 驗證目前使用者是否購買過
MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
long ttl = redisTo.getEndTime() - System.currentTimeMillis();
//3.1 通過在redis中使用 使用者id-skuId 來占位看是否買過
Boolean occupy = redisTemplate.opsForValue().setIfAbsent(memberResponseVo.getId()+"-"+redisTo.getSkuId(), num.toString(), ttl, TimeUnit.MILLISECONDS);
//3.2 占位成功,說明該使用者未秒殺過該商品,則繼續
if (occupy){
//4. 校驗庫存和購買量是否符合要求
if (num <= redisTo.getSeckillLimit()) {
//4.1 嘗試擷取庫存信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());
boolean acquire = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);
//4.2 擷取庫存成功
if (acquire) {
//5. 發送消息建立訂單
//5.1 建立訂單号
orderSn = IdWorker.getTimeId();
//5.2 建立秒殺訂單to
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setMemberId(memberResponseVo.getId());
orderTo.setNum(num);
orderTo.setOrderSn(orderSn);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
orderTo.setSkuId(redisTo.getSkuId());
//5.3 發送建立訂單的消息
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
}
}
}
}
}
return orderSn;
}
複制
5.2 建立訂單
發送消息
//發送建立訂單的消息
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
複制
建立秒殺所需隊列
/**
* 商品秒殺隊列
* @return
*/
@Bean
public Queue orderSecKillOrrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding orderSecKillOrrderQueueBinding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
Binding binding = new Binding(
"order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
return binding;
}
複制
監聽隊列
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class SeckillOrderListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void createOrder(SeckillOrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("***********接收到秒殺消息");
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
orderService.createSeckillOrder(orderTo);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
channel.basicReject(deliveryTag,true);
}
}
}
複制
建立訂單
@Transactional
@Override
public void createSeckillOrder(SeckillOrderTo orderTo) {
MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
//1. 建立訂單
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(orderTo.getOrderSn());
orderEntity.setMemberId(orderTo.getMemberId());
orderEntity.setMemberUsername(memberResponseVo.getUsername());
orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
orderEntity.setCreateTime(new Date());
orderEntity.setPayAmount(orderTo.getSeckillPrice().multiply(new BigDecimal(orderTo.getNum())));
this.save(orderEntity);
//2. 建立訂單項
R r = productFeignService.info(orderTo.getSkuId());
if (r.getCode() == 0) {
SeckillSkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SeckillSkuInfoVo>() {
});
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setOrderSn(orderTo.getOrderSn());
orderItemEntity.setSpuId(skuInfo.getSpuId());
orderItemEntity.setCategoryId(skuInfo.getCatalogId());
orderItemEntity.setSkuId(skuInfo.getSkuId());
orderItemEntity.setSkuName(skuInfo.getSkuName());
orderItemEntity.setSkuPic(skuInfo.getSkuDefaultImg());
orderItemEntity.setSkuPrice(skuInfo.getPrice());
orderItemEntity.setSkuQuantity(orderTo.getNum());
orderItemService.save(orderItemEntity);
}
}
複制
頁面跳轉效果