高並發13-redis+布隆過濾器實現秒殺功能


- 功能核心點

  * 經典互聯網商品搶購秒殺功能

- 功能api

  * 商品秒殺接口

- 數據落地存儲方案

  * 通過分布式redis減庫存
  * DB存最終訂單信息數據

- api性能調優

  * 性能瓶頸在高並發秒殺
  * 技術難題在於超賣問題

 秒殺系統功能步驟梳理

- 利用 Redis 緩存incr攔截流量

  - 首先通過數據控制模塊,提前將秒殺商品緩存到讀寫分離 Redis,並設置秒殺開始標記如下:

    "skuId_start": 0 //開始標記1表示秒殺開始
    "skuId_count": 10000 //總數
    "skuId_access": 12000 //接受搶購數(接受最大搶購數=1.2*商品總數

  - 秒殺開始前,服務集群讀取 skuId_start為 0,直接返回未開始。

  - 服務時間不一致可能導致流量傾斜

  - 數據控制模塊將 skuId_start 改為1,標志秒殺開始。

  - 當接受下單數達到 sku_count*1.2 后,繼續攔截所有請求,商品剩余數量為 0

 

- 利用Redis緩存加速庫存扣量

  "skuId_booked": 10000 //總數0開始10000 通過incr扣減庫存,返回搶購成功

- 將用戶訂單數據寫入mq

- 監聽mq入庫

 

 

 

 

 

秒殺系統功能api實戰(上)

 ** 后端秒殺網關流量攔截層功能開發 **

- 先判斷秒殺是否已經開始
  * 初始化時將key:skuId_start_1 value:0_1554046102存入數據庫中
- 利用 Redis 緩存incr攔截流量
  - 緩存攔截流量代碼編寫
  - 用incr方法原子加
  - 通過原子加判斷當前skuId_access是否達到最大值
  - 思考:是否需要保證獲取到值的時候和incr值兩個命令的原子性
    * 保證原子性的方式,采用lua腳本
    * 采用lua腳本方式保證原子性帶來缺點,性能有所下降
    * 不保證原子性缺點,放入請求量可能大於skuId_access

秒殺系統功能api實戰(中)

** 后端秒殺信息校驗層功能開發布隆過濾器實現重復購買攔截 **

- 訂單信息校驗層

  * 校驗當前用戶是否已經買過這個商品
    - 需要存儲用戶的uid
    - 存數據庫效率太低
    - 存Redis value方式數據太大
    - 存布隆過濾器性能高且數據量小

- 校驗完通過直接返回搶購成功

 

秒殺系統功能api實戰(下)

** 后端秒殺信息校驗層功能開發lua腳本實現庫存扣除**

- 庫存扣除成功,獲取當前最新庫存

- 如果庫存大於0,即馬上進行庫存扣除,並且訪問搶購成功給用戶

- 考慮原子性問題

  * 保證原子性的方式,采用lua腳本
  * 采用lua腳本方式保證原子性帶來缺點,性能有所下降
  * 不保證原子性缺點,放入請求量可能大於預期值
  * 當前扣除庫存場景必須保證原子性,否則會導致超賣

- 返回搶購結果

  * 搶購成功
  * 庫存沒了 ,搶購失敗

初始化庫存數據:

set skuId_count_1 10000
set skuId_access_1 12000 
set skuId_start_1 0_1571125152
set
skuId_booked_1 0
 
        

 

代碼:

 
package com.concurrent.service;


import com.concurrent.util.RedisService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class SeckillService {

    private static final String secStartPrefix = "skuId_start_";
    private static final String secAccess = "skuId_access_";
    private static final String secCount = "skuId_count_";
    private static final String filterName = "skuId_bloomfilter_";
    private static final String bookedName = "skuId_booked_";


    @Resource
    private RedisService redisService;

    public String seckill(int uid, int skuId) {
        //流量攔截層
        //1、判斷秒殺是否開始   0_1554045087    開始標識_開始時間
        String isStart = (String) redisService.get(secStartPrefix + skuId);
        if (StringUtils.isBlank(isStart)) {
            return "還未開始";
        }
        if (isStart.contains("_")) {
            Integer isStartInt = Integer.parseInt(isStart.split("_")[0]);
            Integer startTime = Integer.parseInt(isStart.split("_")[1]);
            if (isStartInt == 0) {
                if (startTime > getNow()) {
                    return "還未開始";
                } else {
                    //代表秒殺已經開始
                    redisService.set(secStartPrefix + skuId, 1+"");
                }
            } else {
                return "系統異常";
            }
        } else {
            if (Integer.parseInt(isStart) != 1) {
                return "系統異常";
            }
        }
        //流量攔截
        String skuIdAccessName = secAccess + skuId;
        Integer accessNumInt = 0;
        String accessNum = (String) redisService.get(skuIdAccessName);
        if(StringUtils.isNotBlank(accessNum)){
            accessNumInt = Integer.parseInt(accessNum);
        }
        String skuIdCountName = secCount + skuId;
        Integer countNumInt = Integer.parseInt((String) redisService.get(skuIdCountName));
        if (countNumInt * 1.2 < accessNumInt) {
            return "搶購已經完成,歡迎下次參與";
        } else {
            redisService.incr(skuIdAccessName);
        }
        //信息校驗層
        if (redisService.bloomFilterExists(filterName + skuId, uid)){
            return "您已經搶購過該商品,請勿重復下發!";
        }else{
            Boolean isSuccess = redisService.getAndIncrLua(bookedName+skuId,skuIdCountName);
            if(isSuccess){
                redisService.bloomFilterAdd(filterName + skuId, uid);
                //TODO 放入消息隊列進行異步入庫
                return "恭喜您搶購成功!!!";
            }else{
                return "搶購結束,歡迎下次參與";
            }

        }


    }

    private long getNow() {
        return System.currentTimeMillis() / 1000;
    }
}

 RedisService.java

package com.concurrent.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.io.InterruptedIOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Created by Administrator on 2018/10/6.
 */
@Service
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    private static double size = Math.pow(2, 32);

    private static final String bloomFilterName = "isVipBloom";



    /**
     *
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            return operations.get(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 自增
     *
     * @param key
     * @return
     */
    public boolean incr(final String key) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.increment(key,1);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public Boolean bloomFilterAdd(int value){
        DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>();
        bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua")));
        bloomAdd.setResultType(Boolean.class);
        List<Object> keyList= new ArrayList<>();
        keyList.add(bloomFilterName);
        keyList.add(value+"");
        Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList);
        return result;
    }
    public Boolean bloomFilterAdd(String bloomName,int value){
        DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>();
        bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua")));
        bloomAdd.setResultType(Boolean.class);
        List<Object> keyList= new ArrayList<>();
        keyList.add(bloomName);
        keyList.add(value+"");
        Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList);
        return result;
    }


    public Boolean bloomFilterExists(int value) {
        DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>();
        bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua")));
        bloomExists.setResultType(Boolean.class);
        List<Object> keyList = new ArrayList<>();
        keyList.add(bloomFilterName);
        keyList.add(value + "");
        Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList);
        return result;
    }

    public Boolean bloomFilterExists(String bloomName,int value) {
        DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>();
        bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua")));
        bloomExists.setResultType(Boolean.class);
        List<Object> keyList = new ArrayList<>();
        keyList.add(bloomName);
        keyList.add(value + "");
        Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList);
        return result;
    }
  public Boolean getAndIncrLua(String key,String key2){
   DefaultRedisScript<Boolean> bloomExists= new DefaultRedisScript<>();
   bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("secKillIncr2.lua")));
   bloomExists.setResultType(Boolean.class);
   List<Object> keyList= new ArrayList<>();
   keyList.add(key);
   keyList.add(key2);
   Boolean result = (Boolean) redisTemplate.execute(bloomExists,keyList);
   return result;
  }
 }

 

secKillIncr.lua腳本:

local lockKey = KEYS[1]
local lockKey2 = KEYS[2]
-- get info
local result_1 = redis.call('GET', lockKey)
local maxCount = redis.call('GET', lockKey2)
if tonumber(result_1) < tonumber(maxCount)
then
local result_2= redis.call('INCR', lockKey)
return result_1
else
return 0
end

controller:

package com.concurrent.controller;


import com.concurrent.SecondKillApplication;
import com.concurrent.service.SeckillService;
import org.springframework.boot.SpringApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class SeckillController {

    @Resource
    private SeckillService seckillService;

    @RequestMapping("/redis/seckill")
    public String secKill(int uid,int skuId){
         return seckillService.seckill(uid,skuId);
    }

    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis()/1000);

    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM