Redis實(shí)現(xiàn)優(yōu)惠券限一單限制詳解
需求:修改秒殺業(yè)務(wù),要求同一個(gè)優(yōu)惠券,一個(gè)用戶只能下一單
我們只需要在增加訂單之前,拿用戶id和優(yōu)惠券id判斷訂單是否已經(jīng)存在,如果存在,說明用戶已經(jīng)購(gòu)買。
代碼實(shí)現(xiàn):
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } Long userId = UserHolder.getUser().getId(); //6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在 //如果存在,則返回錯(cuò)誤信息 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用戶已經(jīng)購(gòu)買!"); } //7. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //7.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2添加用戶id voucherOrder.setUserId(userId); //7.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回訂單id return Result.ok(orderId); } }
但是,還沒完,這種代碼邏輯,在高并發(fā)的情況下還是會(huì)出現(xiàn)一個(gè)人購(gòu)買購(gòu)買多個(gè)的情況:
就是同一時(shí)間,多個(gè)線程來查詢數(shù)據(jù),都沒有查到訂單,都去創(chuàng)建了訂單(高并發(fā)的情況下)
類似超賣問題,所以我們要進(jìn)行上鎖。
這次就用悲觀鎖。
最簡(jiǎn)單的實(shí)現(xiàn)方法,就是把從查詢訂單是否存在到保存訂單返回訂單id這一段代碼塊進(jìn)行封裝成一個(gè)方法,然后在這個(gè)方法上加上synchronized關(guān)鍵字和spring事務(wù)。
如下:
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } return createVoucherOrder(voucherId); } @Transactional public synchronized Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); //6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在 //如果存在,則返回錯(cuò)誤信息 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用戶已經(jīng)購(gòu)買!"); } //7. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //7.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2添加用戶id voucherOrder.setUserId(userId); //7.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回訂單id return Result.ok(orderId); } }
但是,這個(gè)方法就是使用了悲觀鎖,鎖的對(duì)象是整個(gè)類對(duì)象,所有用戶公用一把鎖,就會(huì)導(dǎo)致串行執(zhí)行,從而性能大大降低。
我們可以只鎖上用戶id,讓他每個(gè)用戶獲得一把鎖。
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } Long userId = UserHolder.getUser().getId(); return createVoucherOrder(voucherId); } @Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); //6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在 synchronized (userId.toString().intern()){ //如果存在,則返回錯(cuò)誤信息 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用戶已經(jīng)購(gòu)買!"); } //7. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //7.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2添加用戶id voucherOrder.setUserId(userId); //7.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回訂單id return Result.ok(orderId); } } }
這里鎖上userid時(shí),除了用toString方法轉(zhuǎn)成字符串,還使用intern方法的原因是:
toString方法的底層原理其實(shí)是new一個(gè)String對(duì)象,然后將其變成字符串,如果只鎖上了加toString方法的userid,就有可能出現(xiàn)相同的userid,但是toString底層new出來的String對(duì)象不同,而多分了鎖。所以使用intern方法來直接判斷常量池中的string值是否一致,值一樣的共用一把鎖,這樣就不會(huì)導(dǎo)致多分鎖了。
但是但是,還沒完因?yàn)檫@里我們是加了鎖和事務(wù),但是因?yàn)檫@個(gè)事務(wù)時(shí)Spring進(jìn)行管理的,它會(huì)在我們代碼塊結(jié)束后才會(huì)去執(zhí)行事務(wù),也就是我們釋放鎖的時(shí)候,才會(huì)執(zhí)行事務(wù)。這個(gè)時(shí)候,鎖放開了,就會(huì)有其他線程進(jìn)來,就很有可能出現(xiàn)事務(wù)提交帶上了其他線程。
我們可以這樣進(jìn)行改進(jìn):在本個(gè)方法上進(jìn)行加鎖。
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()){ return createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); //6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在 //如果存在,則返回錯(cuò)誤信息 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用戶已經(jīng)購(gòu)買!"); } //7. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //7.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2添加用戶id voucherOrder.setUserId(userId); //7.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回訂單id return Result.ok(orderId); } }
但是但是但是,還沒完。哈哈
我們只給創(chuàng)建訂單這個(gè)方法(createVoucherOrder)加了事務(wù),但是沒給上面判斷條件的方法加上事務(wù),而我們鎖代碼塊里執(zhí)行的方法,其實(shí)是this.createVoucherOrder()方法,是沒有加事務(wù)的方法調(diào)用的createVoucherOrder()方法,這個(gè)this可不是spring的事務(wù)代理對(duì)象,這就會(huì)導(dǎo)致事務(wù)失效。
解決方法就是,我們只需要拿到代理對(duì)象,然后通過代理對(duì)象調(diào)用我們這個(gè)加了事務(wù)的方法,也就是createVoucherOrder()方法。
使用 AopContext.currentProxy();方法來拿到代理對(duì)象
溫馨提示 :使用這個(gè)方法前要先做兩件事~
1. 記得在配置類似加上@EnableAspectJAutoProxy(exposeProxy = true)注解來暴露這個(gè)代理對(duì)象
2. 加上依賴:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
完整代碼;:
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.aop.framework.AopContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類 * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } Long userId = UserHolder.getUser().getId(); synchronized (userId.toString().intern()){ IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); } } @Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); //6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在 //如果存在,則返回錯(cuò)誤信息 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用戶已經(jīng)購(gòu)買!"); } //7. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //7.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //7.2添加用戶id voucherOrder.setUserId(userId); //7.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //8.返回訂單id return Result.ok(orderId); } }
到此這篇關(guān)于Redis實(shí)現(xiàn)優(yōu)惠券限一單限制詳解的文章就介紹到這了,更多相關(guān)Redis優(yōu)惠券內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis中Zset類型常用命令的實(shí)現(xiàn)
Zset是Redis的一種有序集合數(shù)據(jù)類型,Zset通過壓縮列表和跳躍表兩種底層編碼方式支持小數(shù)據(jù)集和大數(shù)據(jù)集,支持多種操作,包括添加、查詢、刪除元素以及集合運(yùn)算等,具有不同的時(shí)間復(fù)雜度,感興趣的可以了解一下2024-10-10Redis數(shù)據(jù)庫(kù)的數(shù)據(jù)傾斜詳解
Redis,英文全稱是Remote Dictionary Server(遠(yuǎn)程字典服務(wù)),是一個(gè)開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫(kù),需要的朋友可以參考下2023-07-07redis中token失效引發(fā)的一次生產(chǎn)事故
項(xiàng)目再測(cè)試的時(shí)候發(fā)現(xiàn)不定時(shí)token失效,本文主要介紹了redis中token失效引發(fā)的一次生產(chǎn)事故,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03Redis 如何批量設(shè)置過期時(shí)間(PIPLINE的使用)
有時(shí)候我們并不希望redis的key一直存在。例如緩存,驗(yàn)證碼等數(shù)據(jù),我們希望它們能在一定時(shí)間內(nèi)自動(dòng)的被銷毀。本文就詳細(xì)的介紹一下Redis 如何批量設(shè)置過期時(shí)間,感興趣的可以了解一下2021-11-11詳解redis數(shù)據(jù)結(jié)構(gòu)之壓縮列表
這篇文章主要介紹了詳解redis數(shù)據(jù)結(jié)構(gòu)之壓縮列表的相關(guān)資料,壓縮列表在redis中的結(jié)構(gòu)體名稱為ziplist,其是redis為了節(jié)約內(nèi)存而聲明的一種數(shù)據(jù)結(jié)構(gòu),需要的朋友可以參考下2017-05-05