【使用Redis分布式锁实现优惠券秒杀功能】-Redis学习笔记05

前言

本章节主要实现限时、限量优惠券秒杀功能,并利用分布式锁解决《超卖问题》、《一人一单问题》。

一.优惠券下单基本功能实现

1.功能介绍及流程图

优惠券功能
优惠券流程图

2.代码实现

@Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdworker redisIdworker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher==null){
            return Result.fail("优惠券不存在!");
        }
        // 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();
        // 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);
        // 7.返回订单id
        return Result.ok(orderId);
    }

一.超卖问题

1.问题分析

超卖问题线程1,执行完,查询,查到库存为1,判断库存大于0,然后进行扣减库存操作,在这之间,线程2,线程3,也都进行了查询,查询到的库存也都是1,判断库存也都大于0,都进行了扣减库存操作,导致库存只有1个,卖出了3次。

2.解决方案(悲观锁、乐观锁)

悲观锁: 添加同步锁,让线程串行执行
    优点: 简单粗暴
    缺点: 性能一般
乐观锁: 不加锁,在更新时判断是否有其它线程在修改
    优点: 性能好
    缺点: 存在成功率低的问题

悲观锁、乐观锁
悲观锁是通过加锁的方式,让原本并发执行的变成串行执行,保证了线程安全,但是大大降低了执行效率,悲观锁实现较为简单,本文主要研究乐观锁的实现方式。

2.1 乐观锁实现方式-版本号法

版本号法
版本号法: 数据库冗余一个版本字段,每次查询库存的时候,就将这个版本字段也查询出来,在更新的时候,版本号加一,条件加上版本号等于查询到的版本,如果版本被根据了,数据库的update语句就会执行失败。
如上图,线程1,已查询到的库存为1,版本号为1,同时线程2也查询到库存为1,版本号为1,线程1在更新的时候,将版本加1 ,version = version + 1,同时更新条件加上 and version =1,更新完成,version 值变成2,线程2更新操作的时候,同样会将版本加1,version = version + 1,更新条件加上and version =1,但是此时的version已经被线程1更新为2,导致线程2的更新操作会失败,保证了线程安全。

2.2 乐观锁实现方式-CSA法

CSA法
CSA法: 用需要修改的数据本身来判断数据是否已修改,利用库存本身的数据,来代替了版本,如上图线程1查到的库存是1,更新库存时,update语句加上and stock =1,执行成功,库存变成0,线程2,查到库存是1,同样更新库存时,update语句加上and stock =1,但此时数据已经被线程1改为0,导致线程2的更新操作会失败,保证了线程安全。

2.2.1 代码实现

只需要对更新数据库的语句进行修改

 		// 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherId).eq("stock",voucher.getStock())//where id=? and stock=?
                .update();

该方法解决了线程安全问题,但是带来了新的问题,失败率将会大大提升,如库存为100,100个线程并发执行,同时查到了库存为100,更新时,99个线程都会失败,只有一个会成功,按照正常的业务流程,100个库存,100个线程并发执行,应该都会成功,下面对扣减库存的逻辑进一步优化,解决失败率高的问题。
执行update语句时不用and stock =查到的值,只需要将条件改为 and stock >0,就解决了失败率高的问题。

 // 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherId).gt("stock",voucher.getStock())//where id=? and stock > 0
                .update();

二.一人一单

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

1.单机应用下通过synchronized解决一人一单

引入依赖

  <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);
    }
}

具体实现

@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);
        if (voucher == null) {
            return Result.fail("优惠券不存在!");
        }
        // 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.1一人一单
        Long userId = UserHolder.getUser().getId();
        // 同一个用户加锁,不同用户加不同的锁,toString()底层每次都new了一个新的对象,
        // 会造成同一个用户加的是不同的锁
        // intern()方法是去常量池找跟字符串值一样的地址,避免同一个用户加了不同的锁
        synchronized (userId.toString().intern()) {
            // 事务要生效,需要spring对当前类做了动态代理,拿到代理对象,用代理对象做了事务处理,如果用this调用方法,就是用的是
            // 当前对象,造成实务无效,需要获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, voucher, userId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId, SeckillVoucher voucher, Long userId) {
        // 5.2 查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucher).count();
        // 5.3 判断订单是否存在
        if (count > 0) {
            return Result.fail("用户已购买过一次!");
        }
        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock=stock -1
                .eq("voucher_id", voucherId).gt("stock", voucher.getStock())//where id=? and stock > 0
                .update();
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1 订单id
        long orderId = redisIdworker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        // 7.3 设置代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }
}

2.分布式系统下通过Redis分布式锁解决一人一单

分布式系统每个服务会部署很多个实例,每个实例对一个一个独立的JVM,在每个实例内部能通过synchronized实现线程的互斥,但是实例和实例直接就无法试下线程互斥,只能通过分布式锁来解决。分布式锁
分布式锁的实现

2.1 代码实现

锁的接口类

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec
     * @return
     */
    boolean tryLock(long timeoutSec);
    
    /**
     * 释放锁
     */
    void unlock();
}

实现类

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * @auther Kou
 * @date 2022/7/11 22:33
 */
public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 1.获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 2.获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 3.success是Boolean包装类型,而方法的返回值是基本类型boolean,直接返回success会进行拆箱,
        // 如果success是null,就会报空指针
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 1.获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 2.获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 3.判断线程标识和锁标识是否一样
        if (threadId.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

业务代码改造

   @Resource
    private StringRedisTemplate stringRedisTemplate;
    public Result seckillVoucher01(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher == null) {
            return Result.fail("优惠券不存在!");
        }
        // 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.1一人一单
        Long userId = UserHolder.getUser().getId();
        // 6.创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
        // 7.获取锁
        boolean isLock = lock.tryLock(1200);
        // 8.判断是否获取锁成功
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId, voucher, userId);
        } finally {
            // 9.释放锁
            lock.unlock();
        }
    }
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值