Redis實(shí)現(xiàn)優(yōu)惠券限一單限制詳解
需求:修改秒殺業(yè)務(wù),要求同一個優(yōu)惠券,一個用戶只能下一單
我們只需要在增加訂單之前,拿用戶id和優(yōu)惠券id判斷訂單是否已經(jīng)存在,如果存在,說明用戶已經(jīng)購買。

代碼實(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.判斷庫存是否充足
if (voucher.getStock() < 1) {
Result.fail("庫存不充足!");
}
//5.扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("庫存不充足!");
}
Long userId = UserHolder.getUser().getId();
//6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在
//如果存在,則返回錯誤信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶已經(jīng)購買!");
}
//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ā)的情況下還是會出現(xiàn)一個人購買購買多個的情況:
就是同一時間,多個線程來查詢數(shù)據(jù),都沒有查到訂單,都去創(chuàng)建了訂單(高并發(fā)的情況下)
類似超賣問題,所以我們要進(jìn)行上鎖。
這次就用悲觀鎖。
最簡單的實(shí)現(xiàn)方法,就是把從查詢訂單是否存在到保存訂單返回訂單id這一段代碼塊進(jìn)行封裝成一個方法,然后在這個方法上加上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.判斷庫存是否充足
if (voucher.getStock() < 1) {
Result.fail("庫存不充足!");
}
//5.扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("庫存不充足!");
}
return createVoucherOrder(voucherId);
}
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在
//如果存在,則返回錯誤信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶已經(jīng)購買!");
}
//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);
}
}但是,這個方法就是使用了悲觀鎖,鎖的對象是整個類對象,所有用戶公用一把鎖,就會導(dǎo)致串行執(zhí)行,從而性能大大降低。
我們可以只鎖上用戶id,讓他每個用戶獲得一把鎖。
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.判斷庫存是否充足
if (voucher.getStock() < 1) {
Result.fail("庫存不充足!");
}
//5.扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("庫存不充足!");
}
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()){
//如果存在,則返回錯誤信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶已經(jīng)購買!");
}
//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時,除了用toString方法轉(zhuǎn)成字符串,還使用intern方法的原因是:
toString方法的底層原理其實(shí)是new一個String對象,然后將其變成字符串,如果只鎖上了加toString方法的userid,就有可能出現(xiàn)相同的userid,但是toString底層new出來的String對象不同,而多分了鎖。所以使用intern方法來直接判斷常量池中的string值是否一致,值一樣的共用一把鎖,這樣就不會導(dǎo)致多分鎖了。
但是但是,還沒完因?yàn)檫@里我們是加了鎖和事務(wù),但是因?yàn)檫@個事務(wù)時Spring進(jìn)行管理的,它會在我們代碼塊結(jié)束后才會去執(zhí)行事務(wù),也就是我們釋放鎖的時候,才會執(zhí)行事務(wù)。這個時候,鎖放開了,就會有其他線程進(jìn)來,就很有可能出現(xiàn)事務(wù)提交帶上了其他線程。
我們可以這樣進(jìn)行改進(jìn):在本個方法上進(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.判斷庫存是否充足
if (voucher.getStock() < 1) {
Result.fail("庫存不充足!");
}
//5.扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
Result.fail("庫存不充足!");
}
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)存在
//如果存在,則返回錯誤信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶已經(jīng)購買!");
}
//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)建訂單這個方法(createVoucherOrder)加了事務(wù),但是沒給上面判斷條件的方法加上事務(wù),而我們鎖代碼塊里執(zhí)行的方法,其實(shí)是this.createVoucherOrder()方法,是沒有加事務(wù)的方法調(diào)用的createVoucherOrder()方法,這個this可不是spring的事務(wù)代理對象,這就會導(dǎo)致事務(wù)失效。
解決方法就是,我們只需要拿到代理對象,然后通過代理對象調(diào)用我們這個加了事務(wù)的方法,也就是createVoucherOrder()方法。
使用 AopContext.currentProxy();方法來拿到代理對象
溫馨提示 :使用這個方法前要先做兩件事~
1. 記得在配置類似加上@EnableAspectJAutoProxy(exposeProxy = true)注解來暴露這個代理對象
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.判斷庫存是否充足
if (voucher.getStock() < 1) {
Result.fail("庫存不充足!");
}
//5.扣減庫存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0)
.update();
if (!success){
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) {
Long userId = UserHolder.getUser().getId();
//6.根據(jù)優(yōu)惠券id和用戶id判斷訂單是否已經(jīng)存在
//如果存在,則返回錯誤信息
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶已經(jīng)購買!");
}
//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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis消息隊(duì)列實(shí)現(xiàn)秒殺教程
這篇文章主要介紹了Redis消息隊(duì)列實(shí)現(xiàn)秒殺教程,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
Redis實(shí)現(xiàn)信息已讀未讀狀態(tài)提示
這篇文章主要介紹了Redis實(shí)現(xiàn)信息已讀未讀狀態(tài)提示的相關(guān)資料,需要的朋友可以參考下2016-04-04
Redis實(shí)現(xiàn)每日簽到功能(大數(shù)據(jù)量)
在面對百萬級用戶簽到情況下,傳統(tǒng)數(shù)據(jù)庫存儲和判斷會遇到瓶頸,使用Redis的二進(jìn)制數(shù)據(jù)類型可實(shí)現(xiàn)高效的簽到功能,示例代碼展示了如何調(diào)用這些功能,包括當(dāng)天簽到、補(bǔ)簽以及查詢簽到記錄,PHP結(jié)合Redis二進(jìn)制數(shù)據(jù)類型可有效處理大數(shù)據(jù)量下的簽到問題2024-10-10
Redis實(shí)現(xiàn)編碼生成規(guī)則方式
在自動生成編碼時應(yīng)采用“MD+年月日+4位序列號”的規(guī)則,如“MD202310130001”,為避免使用隨機(jī)序列號導(dǎo)致的重復(fù)編碼,建議使用從0開始的自增序列號,此外,使用Redis的incrBy功能實(shí)現(xiàn)序列號自增,可以有效提高效率和降低實(shí)現(xiàn)難度2023-01-01
Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫過程
大家好,本篇文章主要講的是Linux系統(tǒng)下安裝Redis數(shù)據(jù)庫過程,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12

