那幾個(gè)網(wǎng)站可以做h5企業(yè)品牌推廣方案
本博客為個(gè)人學(xué)習(xí)筆記,學(xué)習(xí)網(wǎng)站與詳細(xì)見(jiàn):黑馬程序員Redis入門(mén)到實(shí)戰(zhàn)?P50?- P54?
目錄
優(yōu)惠卷秒殺下單功能實(shí)現(xiàn)
超賣(mài)問(wèn)題
悲觀鎖與樂(lè)觀鎖
實(shí)現(xiàn)CAS法樂(lè)觀鎖
一人一單功能實(shí)現(xiàn)
代碼優(yōu)化?
代碼細(xì)節(jié)分析?
優(yōu)惠卷秒殺下單功能實(shí)現(xiàn)
?
?
Controller層代碼實(shí)現(xiàn)
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Resourceprivate IVoucherOrderService voucherOrderService;@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}
Service層代碼實(shí)現(xiàn)
//接口類(lèi)
public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);
}//實(shí)現(xiàn)類(lèi)
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1.查詢優(yōu)惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.根據(jù)優(yōu)惠券信息判斷秒殺是否開(kāi)始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 秒殺尚未開(kāi)始return Result.fail("秒殺尚未開(kāi)始!");}// 3.判斷秒殺是否已經(jīng)結(jié)束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 秒殺已經(jīng)結(jié)束return Result.fail("秒殺已經(jīng)結(jié)束!");}// 4.判斷庫(kù)存是否充足if (voucher.getStock() < 1) {// 庫(kù)存不足return Result.fail("庫(kù)存不足!");}// 5.扣減庫(kù)存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {// 扣減失敗return Result.fail("庫(kù)存不足!");}// 6.創(chuàng)建訂單VoucherOrder voucherOrder = new VoucherOrder();// 6.1訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2用戶idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3代金卷idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回訂單idreturn Result.ok(orderId);}
}
超賣(mài)問(wèn)題
假設(shè)秒殺優(yōu)惠卷庫(kù)存為1,正常情況搶票邏輯如下圖所示。?
?
假設(shè)秒殺優(yōu)惠卷庫(kù)存為1,高并發(fā)情況搶票邏輯如下圖所示。?
?
悲觀鎖與樂(lè)觀鎖
?
由于悲觀鎖采用串行執(zhí)行線程的方式,因此性能劣于樂(lè)觀鎖。
樂(lè)觀鎖的關(guān)鍵是判斷之前查詢到的數(shù)據(jù)是否被修改過(guò),常見(jiàn)的方式有以下兩種:
1.版本號(hào)法
為每種秒殺優(yōu)惠卷添加版本號(hào)屬性,每當(dāng)該優(yōu)惠卷的庫(kù)存被更改時(shí),令版本號(hào)也發(fā)生變化。因此只需要在查詢庫(kù)存的同時(shí)查詢并記錄版本號(hào),在修改庫(kù)存時(shí),通過(guò)查詢當(dāng)前版本號(hào)并判斷其是否與先前記錄的版本號(hào)一致來(lái)判斷查詢到的庫(kù)存是否已經(jīng)被其他線程修改過(guò)。
?
2.CAS法(即版本號(hào)法的簡(jiǎn)化,利用庫(kù)存代替版本號(hào))
?
實(shí)現(xiàn)CAS法樂(lè)觀鎖
首先查詢秒殺優(yōu)惠卷信息并保存到變量voucher,當(dāng)判斷該優(yōu)惠卷可以被搶購(gòu)后,在修改該優(yōu)惠卷庫(kù)存時(shí),需要滿足當(dāng)前優(yōu)惠卷庫(kù)存與voucher.getStock()相等的條件。?
盡管添加了樂(lè)觀鎖,但在測(cè)試時(shí)發(fā)現(xiàn)用戶搶購(gòu)優(yōu)惠卷的成功率過(guò)低。原因在于,在高并發(fā)的情況下,假設(shè)10個(gè)用戶同時(shí)搶購(gòu)一張優(yōu)惠卷,那么只有一個(gè)用戶能夠搶購(gòu)成功,其他用戶均搶購(gòu)失敗。而在搶票的業(yè)務(wù)場(chǎng)景下,當(dāng)用戶同時(shí)進(jìn)行搶購(gòu)時(shí),只要優(yōu)惠卷庫(kù)存大于0即可進(jìn)行搶購(gòu),因此只需修改判斷條件,在修改庫(kù)存時(shí),判斷當(dāng)前庫(kù)存是否大于0即可。
?
一人一單功能實(shí)現(xiàn)
?
修改Service層實(shí)現(xiàn)類(lèi)中優(yōu)惠卷秒殺下單seckillVoucher方法,代碼(未優(yōu)化版本)如下:
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {//1.查詢優(yōu)惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2.判斷秒殺是否開(kāi)始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {//尚未開(kāi)始return Result.fail("秒殺尚未開(kāi)始");}//3.判斷秒殺是否已經(jīng)結(jié)束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {//尚未開(kāi)始return Result.fail("秒殺已經(jīng)結(jié)束");}//4.判斷庫(kù)存是否充足if (voucher.getStock()<1) {//庫(kù)存不足return Result.fail("庫(kù)存不足");}//5.一人一單Long userId = UserHolder.getUser().getId();//5.1查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判斷是否存在if(count>1){//用戶已經(jīng)購(gòu)買(mǎi)過(guò)了return Result.fail("用戶已經(jīng)購(gòu)買(mǎi)過(guò)一次");}//6.扣減庫(kù)存boolean success=seckillVoucherService.update().setSql("stock = stock - 1") //set stock =stock - 1.eq("voucher_id",voucherId).gt("stock",0)//where id=? and stock > 0.update();if(!success){return Result.fail("庫(kù)存不足!");}//7.創(chuàng)建訂單VoucherOrder voucherOrder = new VoucherOrder();//7.1訂單Idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2用戶IdvoucherOrder.setUserId(userId);//7.3代金券IdvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//8.返回訂單IDreturn Result.ok(orderId);
}
代碼優(yōu)化?
存在的問(wèn)題:盡管代碼經(jīng)過(guò)修改,但在高并發(fā)的情況下仍然存在問(wèn)題,即單個(gè)用戶在同一刻多次下單,多個(gè)線程均查詢到count=0(該用戶未購(gòu)買(mǎi)過(guò)當(dāng)前優(yōu)惠券),此時(shí)多個(gè)線程同時(shí)創(chuàng)建訂單,從而導(dǎo)致一人多單問(wèn)題。
實(shí)現(xiàn):為實(shí)現(xiàn)一人一單功能,我們需要加鎖,相比超賣(mài)問(wèn)題中每個(gè)線程只是對(duì)數(shù)據(jù)進(jìn)行修改,所以可以使用樂(lè)觀鎖,通過(guò)判斷數(shù)據(jù)值是否被修改來(lái)判斷其他線程是否已經(jīng)對(duì)當(dāng)前數(shù)據(jù)進(jìn)行操作。但一人多單的問(wèn)題中,每個(gè)線程需要進(jìn)行添加數(shù)據(jù)而非修改數(shù)據(jù)的操作,即創(chuàng)建新的訂單,所以這種情況下,我們需要使用悲觀鎖。
優(yōu)化后的代碼如下所示:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查詢優(yōu)惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.根據(jù)優(yōu)惠券信息判斷秒殺是否開(kāi)始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 秒殺尚未開(kāi)始return Result.fail("秒殺尚未開(kāi)始!");}// 3.判斷秒殺是否已經(jīng)結(jié)束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 秒殺已經(jīng)結(jié)束return Result.fail("秒殺已經(jīng)結(jié)束!");}// 4.判斷庫(kù)存是否充足if (voucher.getStock() < 1) {// 庫(kù)存不足return Result.fail("庫(kù)存不足!");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 獲取當(dāng)前代理對(duì)象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic synchronized 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) {//用戶已經(jīng)購(gòu)買(mǎi)過(guò)return Result.fail("用戶已經(jīng)購(gòu)買(mǎi)過(guò)了");}// 6.扣減庫(kù)存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") //使用MP,設(shè)置sql語(yǔ)句.eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {// 扣減失敗return Result.fail("庫(kù)存不足!");}// 7.創(chuàng)建訂單VoucherOrder voucherOrder = new VoucherOrder();// 7.1獲取訂單全局idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2用戶idvoucherOrder.setUserId(userId);// 7.3代金卷idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8.返回訂單idreturn Result.ok(orderId);}
}
代碼細(xì)節(jié)分析?
有以下幾個(gè)細(xì)節(jié)我們需要特別注意:
1. 首先,我們將查詢、判斷、創(chuàng)建訂單等過(guò)程封裝成createVoucherOrder方法,但是我們并不能直接對(duì)這個(gè)方法加鎖,如果對(duì)這個(gè)方法加鎖的話將導(dǎo)致創(chuàng)建訂單的業(yè)務(wù)是串行執(zhí)行的,在高并發(fā)多用戶的情況下,性能會(huì)很差。
2. 因?yàn)槭且蝗艘粏喂δ?#xff0c;我們需要做的是對(duì)同一個(gè)用戶創(chuàng)建訂單的流程進(jìn)行加鎖,所以我們根據(jù)用戶ID來(lái)加鎖。我們?yōu)橄嗤挠脩鬒D配置一把鎖,從而將鎖的范圍縮小,因此我們將createVoucherOrder方法進(jìn)行修改,如下所示。
3. 然而,我們需要確保synchronized()傳入的參數(shù)對(duì)象是相同的,synchronized鎖才能起作用。因此我們不傳入查詢得到的userId對(duì)象,因?yàn)槲覀兠看尾樵兒蠖紩?huì)賦值給新的userId對(duì)象,這樣會(huì)導(dǎo)致傳入synchronized()的參數(shù)始終不是同一個(gè)對(duì)象,因此我們采用的是synchronized(userId.toString())。
4.但是,如果我們直接使用userId.toString() 方法,拿到的對(duì)象實(shí)際上是不同的,toString() 的底層邏輯是new出一個(gè)新對(duì)象,因此我們需要使用intern()方法,該方法是從常量池中拿到數(shù)據(jù),能確保我們每次取得的對(duì)象都是相同的。得到的代碼如下所示。
@Transactional
//將創(chuàng)建訂單封裝成一個(gè)方法
public Result createVoucherOrder(Long voucherId) {//獲取用戶IdLong userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()) {查詢、判斷、創(chuàng)建訂單等流程...}
}//錯(cuò)誤情況:synchronized(userId.toString())
5.由于我們是在方法里面加鎖,因此代碼的邏輯是先釋放鎖,再提交事務(wù),因此在高并發(fā)的環(huán)境下可能會(huì)出現(xiàn)安全問(wèn)題(即釋放鎖后,提交事務(wù)還未完成,其他線程又進(jìn)來(lái)了)。所以我們應(yīng)該先提交事務(wù),再釋放鎖。因此,我們應(yīng)該在方法外加鎖,代碼如下。
@Override
public Result seckillVoucher(Long voucherId) {...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {return createVoucherOrder(voucherId);}
}
6.在以上代碼中,我們調(diào)用createVoucherOrder方法,其實(shí)是通過(guò)this.的方式調(diào)用的,而this代表的是當(dāng)前VoucherOrderServiceImpl對(duì)象,并不是createVoucherOrder的代理對(duì)象,因此事務(wù)是不生效的。要想讓事務(wù)生效,我們需要利用代理對(duì)象調(diào)用createVoucherOrder方法,才能使事務(wù)生效,代碼如下。
@Override
public Result seckillVoucher(Long voucherId) {...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 獲取當(dāng)前代理對(duì)象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}
}
此外,我們還需做以下三步。
1.添加動(dòng)態(tài)代理依賴(lài)
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
2.在接口中添加createVoucherOrder(Long voucherId);?
public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}
3.在啟動(dòng)類(lèi)中添加注解,暴露該代理對(duì)象
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}