首頁 > 軟體

Redis實現優惠券限一單限制詳解

2022-12-07 14:00:04

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

我們只需要在增加訂單之前,拿使用者id和優惠券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>
 *  服務實現類
 * </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.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //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.根據優惠券id和使用者id判斷訂單是否已經存在
        //如果存在,則返回錯誤資訊
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            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);
    }
}

但是,還沒完,這種程式碼邏輯,在高並行的情況下還是會出現一個人購買購買多個的情況:

就是同一時間,多個執行緒來查詢資料,都沒有查到訂單,都去建立了訂單(高並行的情況下)

類似超賣問題,所以我們要進行上鎖。

這次就用悲觀鎖。

最簡單的實現方法,就是把從查詢訂單是否存在到儲存訂單返回訂單id這一段程式碼塊進行封裝成一個方法,然後在這個方法上加上synchronized關鍵字和spring事務。

如下:

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>
 *  服務實現類
 * </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.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //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.根據優惠券id和使用者id判斷訂單是否已經存在
        //如果存在,則返回錯誤資訊
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            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);
    }
}

但是,這個方法就是使用了悲觀鎖,鎖的物件是整個類物件,所有使用者公用一把鎖,就會導致序列執行,從而效能大大降低。

我們可以只鎖上使用者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>
 *  服務實現類
 * </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.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //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.根據優惠券id和使用者id判斷訂單是否已經存在
        synchronized (userId.toString().intern()){
            //如果存在,則返回錯誤資訊
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if (count > 0) {
                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);
        } 
    }
}

這裡鎖上userid時,除了用toString方法轉成字串,還使用intern方法的原因是:

toString方法的底層原理其實是new一個String物件,然後將其變成字串,如果只鎖上了加toString方法的userid,就有可能出現相同的userid,但是toString底層new出來的String物件不同,而多分了鎖。所以使用intern方法來直接判斷常數池中的string值是否一致,值一樣的共用一把鎖,這樣就不會導致多分鎖了。

但是但是,還沒完因為這裡我們是加了鎖和事務,但是因為這個事務時Spring進行管理的,它會在我們程式碼塊結束後才會去執行事務,也就是我們釋放鎖的時候,才會執行事務。這個時候,鎖放開了,就會有其他執行緒進來,就很有可能出現事務提交帶上了其他執行緒。

我們可以這樣進行改進:在本個方法上進行加鎖。

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>
 *  服務實現類
 * </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.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //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.根據優惠券id和使用者id判斷訂單是否已經存在
        //如果存在,則返回錯誤資訊
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            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);
    }
}

但是但是但是,還沒完。哈哈

我們只給建立訂單這個方法(createVoucherOrder)加了事務,但是沒給上面判斷條件的方法加上事務,而我們鎖程式碼塊裡執行的方法,其實是this.createVoucherOrder()方法,是沒有加事務的方法呼叫的createVoucherOrder()方法,這個this可不是spring的事務代理物件,這就會導致事務失效。

解決方法就是,我們只需要拿到代理物件,然後通過代理物件呼叫我們這個加了事務的方法,也就是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>
 *  服務實現類
 * </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.獲取優惠券資訊
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //2.判斷是否已經開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
            Result.fail("秒殺尚未開始!");
        }
        //3.判斷是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())){
            Result.fail("秒殺已經結束了!");
        }
        //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.根據優惠券id和使用者id判斷訂單是否已經存在
        //如果存在,則返回錯誤資訊
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            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);
    }
}

到此這篇關於Redis實現優惠券限一單限制詳解的文章就介紹到這了,更多相關Redis優惠券內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com