欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring?Boot?整合Redis?實現(xiàn)優(yōu)惠卷秒殺?一人一單功能

 更新時間:2022年09月19日 11:25:24   作者:Bug?終結者  
這篇文章主要介紹了Spring?Boot?整合Redis?實現(xiàn)優(yōu)惠卷秒殺?一人一單,在分布式系統(tǒng)下,高并發(fā)的場景下,會出現(xiàn)此類庫存超賣問題,本篇文章介紹了采用樂觀鎖來解決,需要的朋友可以參考下

一、什么是全局唯一ID

?全局唯一ID

在分布式系統(tǒng)中,經(jīng)常需要使用全局唯一ID查找對應的數(shù)據(jù)。產生這種ID需要保證系統(tǒng)全局唯一,而且要高性能以及占用相對較少的空間。

全局唯一ID在數(shù)據(jù)庫中一般會被設成主鍵,這樣為了保證數(shù)據(jù)插入時索引的快速建立,還需要保持一個有序的趨勢。

這樣全局唯一ID就需要保證這兩個需求:

  • 全局唯一
  • 趨勢有序

我們的場景是 優(yōu)惠卷秒殺搶購, 當用戶搶購時,就會生成訂單 并保存到 數(shù)據(jù)庫 的訂單表中,而訂單表 如果使用數(shù)據(jù)庫自增ID就會存在以下問題

  • id的規(guī)律性太明顯
  • 受單表數(shù)據(jù)量限制

場景分析:如果我們的id具有太明顯的規(guī)則,用戶或者說商業(yè)對手很容易猜測出來我們的一些敏感信息,比如商城在一天時間內,賣出了多少單,這明顯不合適。

場景分析二: 隨著我們商城規(guī)模越來越大,MySQL 的單表的容量不宜超過500W,數(shù)據(jù)量過大之后,我們要進行拆庫拆表,但拆分表了之后,他們從邏輯上講他們是同一張表,所以他們的id是不能一樣的, 于是乎我們需要保證id的唯一性。

全局ID生成器,是一種在分布式系統(tǒng)下用來生成全局唯一ID的工具,一般要滿足下列特性:

為了增加ID的安全性,我們可以不直接使用Redis自增的數(shù)值,而是拼接一些其它信息:

ID的組合為

  • 符號位: 1bit,永遠為0
  • 時間戳: 31bit,以秒為單位可以使用69年
  • 序列號: 32bit,秒內的計數(shù)器,支持每秒產生 2^32 個 不同ID

?Redis實現(xiàn)全局唯一ID

編寫工具類

@Component
public class RedisIdWorker {
    /**
     * 開始時間戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列號的位數(shù)
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成時間戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列號
        // 2.1.獲取當前日期,精確到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增長
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

測試存入Redis

@Autowired
private RedisIdWorker redisIdWorker;

private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void testWorkerId() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(300);
    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();
    };

    long begin = System.currentTimeMillis();
    for (int i = 0; i < 300; i++) {
        es.submit(task);
    }
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("times = " + (end- begin));
}

這里用到了 CountDownlatch,簡單的介紹一下:

CountDownLatch名為信號槍:主要的作用是同步協(xié)調在多線程的等待于喚醒問題

我們如果沒有CountDownLatch ,那么由于程序是異步的,當異步程序沒有執(zhí)行完時,主線程就已經(jīng)執(zhí)行完了,然后我們期望的是分線程全部走完之后,主線程再走,所以我們此時需要使用到CountDownLatch

CountDownLatch 中有兩個最重要的方法

  • countDown
  • await

await 是阻塞方法,我們擔心線程沒有執(zhí)行完時,main線程就執(zhí)行,所以可以使用await就阻塞主線程, 那么什么時候main線程不在阻塞呢? 當 CountDownLatch 內部維護的變量為0時,就不再阻塞,直接放行。

什么時候 CountDownLatch 維護的變量變?yōu)? 呢,我們只需要調用一次countDown ,內部變量就減少1,我們讓分線程和變量綁定, 執(zhí)行完一個分線程就減少一個變量,當分線程全部走完,CountDownLatch 維護的變量就是0,此時await就不再阻塞,統(tǒng)計出來的時間也就是所有分線程執(zhí)行完后的時間。

二、環(huán)境準備

需要搭建登錄環(huán)境,基礎環(huán)境代碼和sql文件均已上傳 GitCode 鏈接:基礎環(huán)境和SQL

三、實現(xiàn)秒殺下單

添加優(yōu)惠卷

VoucherServiceImpl 核心代碼

@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
    // 該類無代碼,直接MyBatis-Plus繼承實現(xiàn)類 即可,自動完成持久化
    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Override
    public ResultBean<List<Voucher>> queryVoucherOfShop(Long shopId) {
        // 查詢優(yōu)惠券信息
        List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
        // 返回結果
        return ResultBean.create(0, "success", vouchers);
    }

    @Override
    public void addSeckillVoucher(Voucher voucher) {
        // 保存優(yōu)惠券
        save(voucher);
        // 保存秒殺信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
    }
}

VoucherController 接口層

@RestController
@CrossOrigin
@RequestMapping("/voucher")
public class VoucherController {

    @Autowired
    private IVoucherService voucherService;
    
    /**
     * 新增秒殺券
     * @param voucher 優(yōu)惠券信息,包含秒殺信息
     * @return 優(yōu)惠券id
     */
    @PostMapping("seckill")
    public ResultBean addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }
}

編寫下單業(yè)務

VoucherOrderServiceImpl 優(yōu)惠卷訂單核心業(yè)務類

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder>  implements IVoucherOrderService {


    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //1. 查詢優(yōu)惠卷
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2. 判斷秒殺是否開始 開始時間大于當前時間表示未開始搶購
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒殺尚未開始!");
        }
        //3. 判斷秒殺是否結束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒殺已經(jīng)結束!");
        }
        //4. 判斷庫存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("庫存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        //5. 查詢訂單
        //5.1 查詢訂單
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2 判斷并返回
        if (count > 0) {
            return Result.fail("用戶已經(jīng)購買過!");
        }

        //6. 扣減庫存
        boolean success = seckillVoucherService.update().setSql("stock = stock -1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            return Result.fail("庫存不足!");
        }

        //7. 創(chuàng)建訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8. 返回訂單id
        return Result.ok(orderId);
    }
}

VoucherOrderController 接口層

@RestController
@CrossOrigin
@RequestMapping("/voucher_order")
public class VoucherOrderController {

    @Autowired
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

測試搶購秒殺優(yōu)惠卷

ApiFox 新增以下接口

添加秒殺卷

測試返回成功即可。

搶購秒殺優(yōu)惠卷接口

測試無誤,搶購成功!

四、庫存超賣問題

?問題分析

有關超賣問題分析:在我們原有代碼中是這么寫的

 if (voucher.getStock() < 1) {
        // 庫存不足
        return Result.fail("庫存不足!");
    }
    //5,扣減庫存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update();
    if (!success) {
        //扣減庫存
        return Result.fail("庫存不足!");
    }

假設線程1過來查詢庫存,判斷出來庫存大于1,正準備去扣減庫存,但是還沒有來得及去扣減,此時線程2過來,線程2也去查詢庫存,發(fā)現(xiàn)這個數(shù)量一定也大于1,那么這兩個線程都會去扣減庫存,最終多個線程相當于一起去扣減庫存,此時就會出現(xiàn)庫存的超賣問題。

超賣問題是典型的多線程安全問題, 這種情況下常見的解決方案就是 加 鎖:而對于加鎖,我們通常有兩種解決方案

悲觀鎖:

悲觀鎖可以實現(xiàn)對于數(shù)據(jù)的串行化執(zhí)行,比如syn,和lock都是悲觀鎖的代表,同時,悲觀鎖中又可以再細分為公平鎖,非公平鎖,可重入鎖,等等

樂觀鎖:

會有一個版本號,每次操作數(shù)據(jù)會對版本號+1,再提交回數(shù)據(jù)時,會去校驗是否比之前的版本大1 ,如果大1 ,則進行操作成功,這套機制的核心邏輯在于,**如果在操作過程中,版本號只比原來大1 ,那么就意味著操作過程中沒有人對他進行過修改,他的操作就是安全的,**如果不大1,則數(shù)據(jù)被修改過,當然樂觀鎖還有一些變種的處理方式比如cas

樂觀鎖的典型代表:就是CAS,利用CAS進行無鎖化機制加鎖,varNum是操作前讀取的內存值,while中的var1+var2 是預估值,如果預估值 == 內存值,則代表中間沒有被人修改過,此時就將新值去替換 內存值

其中do while 是為了在操作失敗時,再次進行自旋操作,即把之前的邏輯再操作一次。

int varNum;
do {
    varNum = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

我們采用的方式為:

在操作時,對版本號進行+1 操作,然后要求version 如果是1 的情況下,才能操作,那么第一個線程在操作后,數(shù)據(jù)庫中的version變成了2,但是他自己滿足version=1 ,所以沒有問題,此時線程2執(zhí)行,線程2 最后也需要加上條件version =1 ,但是現(xiàn)在由于線程1已經(jīng)操作過了,所以線程2,操作時就不滿足version=1 的條件了,所以線程2無法執(zhí)行成功

? 樂觀鎖解決庫存超賣

加入以下代碼解決超賣問題

之前的方式要修改前后都保持一致,但是這樣我們分析過,成功的概率太低,所以我們的樂觀鎖需要變一下,改成stock大于0 即可

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0

知識拓展

針對CAS中的自旋壓力過大,我們可以使用Longaddr這個類去解決

Java8 提供的一個對AtomicLong改進后的一個類,LongAdder

大量線程并發(fā)更新一個原子性的時候,天然的問題就是自旋,會導致并發(fā)性問題,當然這也比我們直接使用syn來的好

所以利用這么一個類,LongAdder來進行優(yōu)化

如果獲取某個值,則會對cell和base的值進行遞增,最后返回一個完整的值

以上的解決方式,依然有些問題,下面使用Jmeter進行測試

?Jmeter 測試

添加線程組

添加JSON斷言,我們認為返回結果為false的就是請求失敗

在線程組右擊選擇斷言 --> JSON 斷言

加入以下判斷

判斷success字段,值是否為true,是true就是返回成功~ 反之失敗

查看結果樹、HTTP信息請求頭、匯總報告、聚合報告等均在http請求右擊添加即可

啟動,查看返回的結果

查看聚合報告

異常率這么高,再來看數(shù)據(jù)庫

數(shù)量正確,我們再看訂單表

id都一樣,這可不行啊,我們真實場景下,發(fā)放優(yōu)惠卷不會讓一個用戶去搶購所有的訂單秒殺優(yōu)惠卷,這樣商家就太虧了,全讓黃牛給搶走了,這可不行,我們需要限制用戶的搶購數(shù)量。

五、優(yōu)惠卷秒殺 實現(xiàn)一人一單

初步實現(xiàn)

int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
    return Result.fail("用戶已經(jīng)購買過!");
}

存在問題:現(xiàn)在的問題還是和之前一樣,并發(fā)過來,查詢數(shù)據(jù)庫,都不存在訂單,所以我們還是需要加鎖,但是樂觀鎖比較適合更新數(shù)據(jù),而現(xiàn)在是插入數(shù)據(jù),所以我們需要使用悲觀鎖操作

注意:在這里提到了非常多的問題,我們需要慢慢的來思考,首先我們的初始方案是封裝了一個createVoucherOrder方法,同時為了確保他線程安全,在方法上添加了一把synchronized 鎖

加上悲觀鎖

@Override
    public Result seckillVoucher(Long voucherId) {
        //1. 查詢優(yōu)惠卷
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2. 判斷秒殺是否開始 開始時間大于當前時間表示未開始搶購
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒殺尚未開始!");
        }
        //3. 判斷秒殺是否結束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒殺已經(jīng)結束!");
        }
        //4. 判斷庫存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("庫存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, userId);
        }
    }

    @Transactional
    @Override
    public Result createVoucherOrder(Long voucherId, Long userId) {
        //5. 查詢訂單
        //5.1 查詢訂單
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2 判斷并返回
        if (count > 0) {
            return Result.fail("用戶已經(jīng)購買過!");
        }

        //6. 扣減庫存
        boolean success = seckillVoucherService.update().setSql("stock = stock -1")
                .eq("voucher_id", voucherId).gt("stock", 0).
                update();
        if (!success) {
            return Result.fail("庫存不足!");
        }


        //7. 創(chuàng)建訂單
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        //8. 返回訂單id
        return Result.ok(orderId);
    }

在啟動類加入以下注解,啟動AspectJ

@EnableAspectJAutoProxy(exposeProxy = true)

以上代碼,采用悲觀鎖解決了高并發(fā)下,一人多單的場景,同時,也解決了事務失效。引入了AspectJ解決!

Jmeter 測試

再次測試,查看結果

可見返回的結果異常率如此高,再看請求信息

可見已經(jīng)成功的攔截了錯誤請求,JSON斷言正確。

查看數(shù)據(jù)庫 信息

優(yōu)惠卷數(shù)量

可見成功的完成了 在高并發(fā)請求下 的一人一單功能。

?小結

以上就是【Bug 終結者】對 微服務Spring Boot 整合Redis 實現(xiàn)優(yōu)惠卷秒殺 一人一單 的簡單介紹,在分布式系統(tǒng)下,高并發(fā)的場景下,會出現(xiàn)此類庫存超賣問題,本篇文章介紹了采用樂觀鎖來解決,但是依然是有弊端,下章節(jié),我們將繼續(xù)進行優(yōu)化,持續(xù)關注!

到此這篇關于Spring Boot 整合Redis 實現(xiàn)優(yōu)惠卷秒殺 一人一單的文章就介紹到這了,更多相關Spring Boot 整合Redis 優(yōu)惠卷秒殺內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 利用Supervisor管理Redis進程的方法教程

    利用Supervisor管理Redis進程的方法教程

    Supervisor 是可以在類 UNIX 系統(tǒng)中進行管理和監(jiān)控各種進程的小型系統(tǒng)。它自帶了客戶端和服務端工具,下面這篇文章主要給大家介紹了關于利用Supervisor管理Redis進程的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧。
    2017-08-08
  • redis基本類型和使用方法詳解

    redis基本類型和使用方法詳解

    這篇文章主要介紹了redis基本類型和使用方法詳解,需要的朋友可以參考下
    2020-02-02
  • Redis事務處理的使用操作方法

    Redis事務處理的使用操作方法

    Redis保證一個事務中的所有命令要么都執(zhí)行,要么都不執(zhí)行(原子性),如果客戶端發(fā)送了EXEC命令,所有的命令就都會被執(zhí)行,即使此后客戶端斷線也沒關系,因為Redis中已經(jīng)記錄了所有要執(zhí)行的命令,下面通過本文給大家介紹Redis事務處理的使用操作,感興趣的朋友一起看看吧
    2021-10-10
  • Redis之常用數(shù)據(jù)結構哈希表

    Redis之常用數(shù)據(jù)結構哈希表

    這篇文章主要介紹了Redis常用的數(shù)據(jù)結構哈希表,哈希表是一種保存鍵值對的數(shù)據(jù)結構,具有一定的參考價值,需要的朋友可以參考閱讀
    2023-04-04
  • 淺談redis緩存在項目中的使用

    淺談redis緩存在項目中的使用

    最近由于項目需要,在系統(tǒng)緩存服務部分上用到了redis,本文就淺談下在redis緩存在項目中的使用,感興趣的小伙伴們可以參考一下
    2021-05-05
  • Redis集群指定主從關系及動態(tài)增刪節(jié)點方式

    Redis集群指定主從關系及動態(tài)增刪節(jié)點方式

    這篇文章主要介紹了Redis集群指定主從關系及動態(tài)增刪節(jié)點方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • 如何利用Redis作為Mybatis的二級緩存

    如何利用Redis作為Mybatis的二級緩存

    這篇文章主要介紹了如何利用Redis作為Mybatis的二級緩存,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,需要的朋友可以參考一下
    2022-08-08
  • Redis高并發(fā)場景下秒殺超賣解決方案(秒殺場景)

    Redis高并發(fā)場景下秒殺超賣解決方案(秒殺場景)

    早起的12306購票,剛被開發(fā)出來使用的時候,12306會經(jīng)常出現(xiàn)超賣 這種現(xiàn)象,也就是說車票只剩10張了,卻被20個人買到了,這種現(xiàn)象就是超賣,今天通過本文給大家介紹Redis高并發(fā)場景下秒殺超賣解決方案,感興趣的朋友一起看看吧
    2022-04-04
  • React?組件的常用生命周期函數(shù)匯總

    React?組件的常用生命周期函數(shù)匯總

    這篇文章主要介紹了React?組件的常用生命周期函數(shù)匯總,組件的生命周期有助于理解組件的運行方式、完成更復雜的組件功能、分析組件錯誤原因等
    2022-08-08
  • Windows下Redis安裝配置簡單教程

    Windows下Redis安裝配置簡單教程

    這篇文章主要為大家詳細介紹了Windows下Redis安裝配置簡單教程,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12

最新評論