中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Redis優惠券秒殺功能怎么實現

發布時間:2022-08-16 16:20:52 來源:億速云 閱讀:193 作者:iii 欄目:開發技術

本篇內容主要講解“Redis優惠券秒殺功能怎么實現”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Redis優惠券秒殺功能怎么實現”吧!

    一、全局唯一ID

    1. 全局ID生成器

    每個店鋪都可以發布優惠券:

    Redis優惠券秒殺功能怎么實現

    當用戶搶購時,就會生成訂單并保存到tb_voucher_order這張表中,而訂單表如果使用數據庫自增ID就存在一些問題:

    • id的規律性太明顯

    • 受單表數據量的限制

    所以tb_voucher_order表的主鍵不能用自增ID:

    create table tb_voucher_order
    (
        id          bigint                                        not null comment '主鍵'
            primary key,
        user_id     bigint unsigned                               not null comment '下單的用戶id',
        voucher_id  bigint unsigned                               not null comment '購買的代金券id',
        pay_type    tinyint(1) unsigned default 1                 not null comment '支付方式 1:余額支付;2:支付寶;3:微信',
        status      tinyint(1) unsigned default 1                 not null comment '訂單狀態,1:未支付;2:已支付;3:已核銷;4:已取消;5:退款中;6:已退款',
        create_time timestamp           default CURRENT_TIMESTAMP not null comment '下單時間',
        pay_time    timestamp                                     null comment '支付時間',
        use_time    timestamp                                     null comment '核銷時間',
        refund_time timestamp                                     null comment '退款時間',
        update_time timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新時間'
    );

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

    Redis優惠券秒殺功能怎么實現

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

    Redis優惠券秒殺功能怎么實現

    D的組成部分:

    • 符號位:1bit,永遠為0,表示正數

    • 時間戳:31bit,以秒為單位,可以使用69年

    • 序列號:32bit,秒內的計數器,支持每秒產生2^32個不同ID

    編寫全局ID生成器代碼:

    @Component
    public class RedisIdWorker {
        /**
         * 開始時間戳,以2022.1.1為基準計算時間差
         */
        private static final long BEGIN_TIMESTAMP = 1640995200L;
        /**
         * 序列號的位數
         */
        private static final int COUNT_BITS = 32;
    
        private StringRedisTemplate stringRedisTemplate;
    
        public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
        }
    
        /**
         * 生成帶有業務前綴的redis自增id
         * @param keyPrefix
         * @return
         */
        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.拼接并返回
            // 用于是數字類型的拼接,所以不能像拼接字符串那樣處理,而是通過位運算將高32位存 符號位+時間戳,低32位存 序列號
            return timestamp << COUNT_BITS | count;
        }
    
        public static void main(String[] args) {
            LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
            long second = time.toEpochSecond(ZoneOffset.UTC);
            System.out.println(second);// 1640995200
        }
    }

    測試全局ID生成器:

    @SpringBootTest
    class HmDianPingApplicationTests {
    	@Resource
        private RedisIdWorker redisIdWorker;
    
        private ExecutorService executorService = Executors.newFixedThreadPool(500);
    
    	@Test
        void testIdWorker() throws InterruptedException {
            CountDownLatch latch = new CountDownLatch(300);
    
            // 每個線程生成100個id
            Runnable task = () -> {
                for (int i = 0; i < 100; i++) {
                    long id = redisIdWorker.nextId("order");
                    System.out.println("id = " + id);
                }
                latch.countDown();
            };
            // 300個線程
            long begin = System.currentTimeMillis();
            for (int i = 0; i < 300; i++) {
                executorService.submit(task);
            }
            latch.await();
            long end = System.currentTimeMillis();
            System.out.println("time = " + (end - begin));
        }
    }

    測試結果:

    Redis優惠券秒殺功能怎么實現

    Redis優惠券秒殺功能怎么實現

    2. 全局唯一ID生成策略

    • UUID(不是遞增的)

    • Redis自增

    • 雪花算法(snowflake)

    • 數據庫自增(單獨建一張表存自增id,分配到分庫分表后的表中)

    3. Redis自增ID策略

    • 以日期作為前綴的key,方便統計訂單量

    • 自增ID的結構:時間戳 + 計數器

    二、實現優惠券秒殺下單

    1. 添加優惠券

    每個店鋪都可以發布優惠券,分為平價券和特價券。平價券可以任意購買,而特價券需要秒殺搶購:

    Redis優惠券秒殺功能怎么實現

    優惠券表信息:

    • tb_voucher:優惠券的基本信息,優惠金額、使用規則等(tb_voucher表的type字段區分是普通券還是秒殺券)

    • tb_seckill_voucher:優惠券的庫存、開始搶購時間,結束搶購時間(秒殺券才需要填寫這些信息),同時秒殺券擁有普通券的基本信息(秒殺券表tb_seckill_voucher的主鍵id綁定的是普通券表tb_voucher的id)

    create table tb_voucher
    (
        id           bigint unsigned auto_increment comment '主鍵'
            primary key,
        shop_id      bigint unsigned                               null comment '商鋪id',
        title        varchar(255)                                  not null comment '代金券標題',
        sub_title    varchar(255)                                  null comment '副標題',
        rules        varchar(1024)                                 null comment '使用規則',
        pay_value    bigint(10) unsigned                           not null comment '支付金額,單位是分。例如200代表2元',
        actual_value bigint(10)                                    not null comment '抵扣金額,單位是分。例如200代表2元',
        type         tinyint(1) unsigned default 0                 not null comment '0,普通券;1,秒殺券',
        status       tinyint(1) unsigned default 1                 not null comment '1,上架; 2,下架; 3,過期',
        create_time  timestamp           default CURRENT_TIMESTAMP not null comment '創建時間',
        update_time  timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新時間'
    );
    create table tb_seckill_voucher
    (
        voucher_id  bigint unsigned                     not null comment '關聯的優惠券的id'
            primary key,
        stock       int(8)                              not null comment '庫存',
        create_time timestamp default CURRENT_TIMESTAMP not null comment '創建時間',
        begin_time  timestamp default CURRENT_TIMESTAMP not null comment '生效時間',
        end_time    timestamp default CURRENT_TIMESTAMP not null comment '失效時間',
        update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新時間'
    )
        comment '秒殺優惠券表,與優惠券是一對一關系';

    2. 編寫添加秒殺券的接口

    主要代碼:

    @RestController
    @RequestMapping("/voucher")
    public class VoucherController {
    
        @Resource
        private IVoucherService voucherService;
    
        /**
         * 新增秒殺券
         * @param voucher 優惠券信息,包含秒殺信息
         * @return 優惠券id
         */
        @PostMapping("seckill")
        public Result addSeckillVoucher(@RequestBody Voucher voucher) {
            voucherService.addSeckillVoucher(voucher);
            return Result.ok(voucher.getId());
        }
    }
    @Service
    public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
    
        @Resource
        private ISeckillVoucherService seckillVoucherService;
    
        @Override
        @Transactional
        public void addSeckillVoucher(Voucher voucher) {
            // 保存優惠券
            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);
        }
    }

    測試添加:

    Redis優惠券秒殺功能怎么實現

    測試結果:

    Redis優惠券秒殺功能怎么實現

    三、實現秒殺下單

    Redis優惠券秒殺功能怎么實現

    下單時需要判斷兩點:

    • 秒殺是否開始或結束,如果尚未開始或已經結束則無法下單

    • 庫存是否充足,不足則無法下單

    Redis優惠券秒殺功能怎么實現

    主要代碼:

    @RestController
    @RequestMapping("/voucher-order")
    public class VoucherOrderController {
        @Resource
        private IVoucherOrderService voucherOrderService;
    
        @PostMapping("seckill/{id}")
        public Result seckillVoucher(@PathVariable("id") Long voucherId) {
            return voucherOrderService.seckillVoucher(voucherId);
        }
    }
    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
        @Resource
        private ISeckillVoucherService seckillVoucherService;
    
        @Resource
        private RedisIdWorker redisIdWorker;
    
        @Override
        public Result seckillVoucher(Long voucherId) {
            // 1.查詢優惠券
            SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
            // 2.判斷秒殺是否開始
            if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
                // 尚未開始
                return Result.fail("秒殺尚未開始!");
            }
            // 3.判斷秒殺是否已經結束
            if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
                // 尚未開始
                return Result.fail("秒殺已經結束!");
            }
            // 4.判斷庫存是否充足
            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("庫存不足!");
            }
            // 6.創建訂單
            VoucherOrder voucherOrder = new VoucherOrder();
            // 6.1.訂單id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 6.2.用戶id
            Long userId = UserHolder.getUser().getId();
            voucherOrder.setUserId(userId);
            // 6.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
    
            return Result.ok(orderId);
    
        }
    }

    簡單測試秒殺成功:

    Redis優惠券秒殺功能怎么實現

    扣減庫存成功:

    Redis優惠券秒殺功能怎么實現

    四、超賣問題

    當有大量請求同時訪問時,就會出現超賣問題

    Redis優惠券秒殺功能怎么實現

    Redis優惠券秒殺功能怎么實現

    超賣問題是典型的多線程安全問題,針對這一問題的常見解決方案就是加鎖:

    Redis優惠券秒殺功能怎么實現

    1. 加鎖方式 - 樂觀鎖

    樂觀鎖的關鍵是判斷之前查詢得到的數據是否有被修改過,常見的方式有兩種:

    (1)版本號法

    Redis優惠券秒殺功能怎么實現

    (2)CAS法

    • 用庫存代替了版本號,可以少加一個字段

    • 扣庫存時,與查詢時的庫存比較,沒被修改則可以扣減庫存

    Redis優惠券秒殺功能怎么實現

    2. 樂觀鎖解決超賣問題

    樂觀鎖方式,通過CAS判斷前后庫存是否一致,解決超賣問題:

    // 之前的代碼
    boolean success = seckillVoucherService.update()
                    .setSql("stock= stock -1")
                    .eq("voucher_id", voucherId).update();
                    
    // 樂觀鎖方式,通過CAS判斷前后庫存是否一致,解決超賣問題                
    boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1") // set stock = stock -1
                .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); // where id = ? and stock = ?

    又出現新的問題:

    • 假設100個線程同時請求,但通過CAS判斷后,只有一個線程能扣減庫存成功,其余99個線程全部失敗

    • 此時,庫存剩余99,但是實際業務可以滿足其余99個線程扣減庫存

    • 雖然能解決超賣問題,但是設計不合理

    所以為了解決失敗率高的問題,需要進一步改進:

    通過CAS 不再 判斷前后庫存是否一致,而是判斷庫存是否大于0

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

    3. 小結

    超賣這樣的線程安全問題,解決方案有哪些?
    (1)悲觀鎖:添加同步鎖,讓線程串行執行

    • 優點:簡單粗暴

    • 缺點:性能一般

    (2)樂觀鎖:不加鎖,在更新時判斷是否有其它線程在修改

    • 優點:性能相對悲觀鎖好(但是仍然需要同時查數據庫,影響性能)

    • 缺點:存在成功率低的問題(可以采用分段鎖方式提高成功率)

    五、一人一單問題

    需求:修改秒殺業務,要求同一個優惠券,一個用戶只能下一單

    Redis優惠券秒殺功能怎么實現

    在扣減庫存之前,加上一人一單的邏輯:

    // 5.一人一單邏輯
    Long userId = UserHolder.getUser().getId();
      // 5.1.查詢訂單數量
      int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
      // 5.2.判斷是否下過單
      if (count > 0) {
          // 用戶已經購買過了
          return Result.fail("用戶已經購買過一次!");
      }

    此處仍會出現并發問題,當同一用戶模擬大量請求同時查詢是否下過單時,如果正好都查詢出count為0,就會跳過判斷繼續執行扣減庫存的邏輯,此時就會出現一人下多單的問題

    解決方法:

    • 由于是判斷查詢的數據是否存在,而不是像之前判斷查詢的數據是否修改過

    • 所以這里只能加悲觀鎖

    1. 加鎖分析

    • 首先將一人一單之后的邏輯全部加鎖,所以將一人一單之后的邏輯抽取出一個方法進行加鎖,public Result createVoucherOrder(Long voucherId)

    • 如果直接在方法上加鎖,則鎖的是this對象,鎖的對象粒度過大,就算是不同的人執行都會阻塞住,影響性能,public synchronized Result createVoucherOrder(Long voucherId)

    • 所以將鎖的對象改為userId,但是不能直接使用synchronized (userId),因為每次執行Long userId = UserHolder.getUser().getId();雖然值一樣,但是對象不同,因此需要這樣加鎖 synchronized (userId.toString().intern()),intern()表示每次從字符串常量池中獲取,這樣值相同時,對象也相同

    • 為了防止事務還沒提交就釋放鎖的問題,則不能將鎖加在createVoucherOrder方法內部,例如:

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
    	synchronized (userId.toString().intern()) {
    		。。。
    	}
    }

    而是需要等事務提交完再釋放鎖,例如:

    synchronized (userId.toString().intern()) {
     	// 獲取代理對象(事務)
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }

    2. 事務分析

    由于只有一人一單之后的邏輯涉及到修改數據庫,所以只需對該方法加事務
    @Transactional
    public Result createVoucherOrder(Long voucherId)

    由于只對createVoucherOrder方法加了事務,而該方法是在seckillVoucher方法中被調用,seckillVoucher方法又沒有加事務,為了防止事務失效,則不能直接在seckillVoucher方法調用createVoucherOrder方法,例如:

    @Override
    public Result seckillVoucher(Long voucherId) {
    	。。。。
    	synchronized (userId.toString().intern()) {
            return this.createVoucherOrder(voucherId);
        }
    }

    而是需要通過代理對象調用createVoucherOrder方法,因為@Transactional事務注解的原理是通過獲取代理對象執行目標對象的方法,進行AOP操作,所以需要這樣:

    @Override
    public Result seckillVoucher(Long voucherId) {
    	。。。。
    	// 獲取代理對象(事務)
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }

    并且還要引入依賴:

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>

    還要開啟注解暴露出代理對象:

    @EnableAspectJAutoProxy(exposeProxy = true)
    @MapperScan("com.hmdp.mapper")
    @SpringBootApplication
    public class HmDianPingApplication {
        public static void main(String[] args) {
            SpringApplication.run(HmDianPingApplication.class, args);
        }
    }

    完整VoucherOrderServiceImpl代碼:

    @Service
    public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    
        @Resource
        private ISeckillVoucherService seckillVoucherService;
    
        @Resource
        private RedisIdWorker redisIdWorker;
    
        @Override
        public Result seckillVoucher(Long voucherId) {
            // 1.查詢優惠券
            SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
            // 2.判斷秒殺是否開始
            if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
                // 尚未開始
                return Result.fail("秒殺尚未開始!");
            }
            // 3.判斷秒殺是否已經結束
            if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
                // 尚未開始
                return Result.fail("秒殺已經結束!");
            }
            // 4.判斷庫存是否充足
            if (voucher.getStock() < 1) {
                // 庫存不足
                return Result.fail("庫存不足!");
            }
    
            Long userId = UserHolder.getUser().getId();
            synchronized (userId.toString().intern()) {
                // 獲取代理對象(事務)
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
                return proxy.createVoucherOrder(voucherId);
            }
            
        }
    
        @Transactional
        public Result createVoucherOrder(Long voucherId) {
    
            // 5.一人一單邏輯
            Long userId = UserHolder.getUser().getId();
            // 5.1.查詢訂單數量
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判斷是否下過單
            if (count > 0) {
                // 用戶已經購買過了
                return Result.fail("用戶已經購買過一次!");
            }
    
            // 6,扣減庫存
            // 樂觀鎖方式,通過CAS判斷庫存是否大于0,解決超賣問題:
            boolean success = seckillVoucherService.update()
                    .setSql("stock= stock -1")
                    .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0
    
            if (!success) {
                // 扣減庫存失敗
                return Result.fail("庫存不足!");
            }
    
            // 7.創建訂單
            VoucherOrder voucherOrder = new VoucherOrder();
            // 7.1.訂單id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 7.2.用戶id
            voucherOrder.setUserId(userId);
            // 7.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
    
            // 8.返回訂單id
            return Result.ok(orderId);
        }
        
    }

    六、集群模式下并發安全問題

    通過加鎖可以解決在單機情況下的一人一單安全問題,但是在集群模式下就不行了。

    我們將服務啟動兩份,端口分別為8081和8082:

    Redis優惠券秒殺功能怎么實現

    然后修改nginx的conf目錄下的nginx.conf文件,配置反向代理和負載均衡

    Redis優惠券秒殺功能怎么實現

    修改完后,重新加載nginx配置文件:

    Redis優惠券秒殺功能怎么實現

    現在,用戶請求會在這兩個節點上負載均衡,再次測試下是否存在線程安全問題:

    訪問8081端口的線程進入了synchronized中

    Redis優惠券秒殺功能怎么實現

    訪問8082端口的線程也進入了synchronized中

    Redis優惠券秒殺功能怎么實現

    最終同一個用戶下了2單扣了2個庫存,所以在集群模式下,出現了一人多單的問題:

    Redis優惠券秒殺功能怎么實現

    分析:

    • 鎖的原理是每個JVM中都有一個Monitor作為鎖對象,所以當對象相同時,獲取的就是同一把鎖

    • 但是不同的JVM中的Monitor不同,所以獲取的不是同一把鎖

    • 因此集群模式下,加synchronized鎖也會出現并發安全問題,需要加分布式鎖

    Redis優惠券秒殺功能怎么實現

    到此,相信大家對“Redis優惠券秒殺功能怎么實現”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    炎陵县| 大理市| 义马市| 宜君县| 楚雄市| 郎溪县| 沁源县| 云浮市| 响水县| 八宿县| 柘荣县| 通渭县| 石城县| 威远县| 吐鲁番市| 高青县| 大足县| 三江| 河津市| 九龙县| 清苑县| 新安县| 常宁市| 平远县| 武穴市| 绥宁县| 景德镇市| 怀柔区| 凤山县| 武城县| 二连浩特市| 兴国县| 阳曲县| 洛南县| 金门县| 西乡县| 多伦县| 汨罗市| 富民县| 额济纳旗| 鲁山县|