天天看點

谷粒商城-進階篇(秒殺功能)

一、秒殺(高并發)系統關注的問題

谷粒商城-進階篇(秒殺功能)
谷粒商城-進階篇(秒殺功能)
秒殺業務:

​ 秒殺具有瞬間高并發的特點,針對這一特點,必須要做限流 + 異步 + 緩存(頁面靜态化)+ 獨立部署

限流方式:
1. 前端限流,一些高并發的網站直接在前端頁面開始限流,例如:小米的驗證碼設計           

複制

  1. nginx,限流,直接負載部分請求到錯誤的靜态頁面:令牌算法漏鬥算法
  2. 網關限流,限流的過濾器
  3. 代碼中使用分布式信号量
  4. 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

  • 使用定時任務每天三點上架最新秒殺商品,削減高峰期壓力
  • 秒殺連結加密,為秒殺商品添加唯一商品随機碼,在開始秒殺時才暴露接口
  • 庫存預熱,先從資料庫中扣除一部分庫存以

    redisson 信号量

    的形式存儲在 redis 中
  • 隊列削峰,秒殺成功後立即傳回,然後以發送消息的形式建立訂單
谷粒商城-進階篇(秒殺功能)

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 首頁展示秒殺活動

谷粒商城-進階篇(秒殺功能)
  • 業務說明和邏輯分析

    業務說明:展示符合目前頁面時間的秒殺活動,把關聯的商品都顯示出來

    邏輯分析:

    1. 判斷目前時間是否落在了活動資訊 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

    2. 傳回商品資訊的時候,要屏蔽掉随機碼資訊【這個業務還是需要的,在商品頁,如果目前商品參與了秒殺,不傳回随機碼資訊】
@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);
    }
}           

複制

頁面跳轉效果
谷粒商城-進階篇(秒殺功能)