<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
每個店鋪都可以釋出優惠券:
當用戶搶購時,就會生成訂單並儲存到tb_voucher_order這張表中,而訂單表如果使用資料庫自增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的工具,一般要滿足下列特性:
為了增加ID的安全性,我們可以不直接使用Redis自增的數值,而是拼接一些其它資訊:
D的組成部分:
編寫全域性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)); } }
測試結果:
每個店鋪都可以釋出優惠券,分為平價券和特價券。平價券可以任意購買,而特價券需要秒殺搶購:
優惠券表資訊:
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 '秒殺優惠券表,與優惠券是一對一關係';
主要程式碼:
@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); } }
測試新增:
測試結果:
下單時需要判斷兩點:
主要程式碼:
@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); } }
簡單測試秒殺成功:
扣減庫存成功:
當有大量請求同時存取時,就會出現超賣問題
超賣問題是典型的多執行緒安全問題,針對這一問題的常見解決方案就是加鎖:
樂觀鎖的關鍵是判斷之前查詢得到的資料是否有被修改過,常見的方式有兩種:
(1)版本號法
(2)CAS法
樂觀鎖方式,通過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 = ?
又出現新的問題:
所以為了解決失敗率高的問題,需要進一步改進:
通過CAS 不再 判斷前後庫存是否一致,而是判斷庫存是否大於0
boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0
超賣這樣的執行緒安全問題,解決方案有哪些?
(1)悲觀鎖:新增同步鎖,讓執行緒序列執行
(2)樂觀鎖:不加鎖,在更新時判斷是否有其它執行緒在修改
需求:修改秒殺業務,要求同一個優惠券,一個使用者只能下一單
在扣減庫存之前,加上一人一單的邏輯:
// 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,就會跳過判斷繼續執行扣減庫存的邏輯,此時就會出現一人下多單的問題
解決方法:
@Transactional public Result createVoucherOrder(Long voucherId) { synchronized (userId.toString().intern()) { 。。。 } }
而是需要等事務提交完再釋放鎖,例如:
synchronized (userId.toString().intern()) { // 獲取代理物件(事務) IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
由於只有一人一單之後的邏輯涉及到修改資料庫,所以只需對該方法加事務
@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:
然後修改nginx的conf目錄下的nginx.conf檔案,設定反向代理和負載均衡:
修改完後,重新載入nginx組態檔:
現在,使用者請求會在這兩個節點上負載均衡,再次測試下是否存線上程安全問題:
存取8081埠的執行緒進入了synchronized中
存取8082埠的執行緒也進入了synchronized中
最終同一個使用者下了2單扣了2個庫存,所以在叢集模式下,出現了一人多單的問題:
分析:
到此這篇關於Redis優惠券秒殺企業實戰的文章就介紹到這了,更多相關Redis 優惠券秒殺 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45