Redis解决优惠券秒杀应用案例

目录
  • 【前端页面】
  • 【分析代码】
  • 一人一单
  • 展望

虽然本文是针对黑马点评的优惠券秒杀业务的实现,但是是适用于各种抢购活动,保证线程安全。

摘要:本文先讲了抢购问题,指出其中会出现的多线程问题,提出解决方案采用悲观锁和乐观锁两种方式进行实现,然后发现在抢购过程中容易出现一人多单现象,为保证优惠券不会被【黄牛】抢到,因此我们在保证多线程安全的情况下实现了一人一单业务,最后指出本文的实现在集群情况下的不足之处。在本专栏的另一篇文章中提出集群或者分布式系统的解决方案

【前端页面】

在代金券发放后,多个用户会进行优惠券抢购,在抢购时需要判断两点:

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

下单核心逻辑分析:

当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件

比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

【逻辑图】

【代码实现】

@Override
public Result seckillVoucher(Long voucherId) {
    // 1.查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 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();
    if (!success) {
        //扣减库存
        return Result.fail("库存不足!");
    }
    //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);

    return Result.ok(orderId);

}

【分析代码】

  • 从上述的逻辑图中我们可以知道,要扣减库存,并且要保存订单,因此需要事务业务
  • 在第4步判断库存是否充足处,会出现多线程问题。出现订单超卖现象

问题代码如下:

 if (voucher.getStock() < 1) {
        // 库存不足
        return Result.fail("库存不足!");
    }
    //5,扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update();
    if (!success) {
        //扣减库存
        return Result.fail("库存不足!");
    }

【采用锁】解决上述超卖问题。

悲观锁:

悲观锁可以实现对于数据的串行化执行,比如syn,和lock都是悲观锁的代表,同时,悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等

乐观锁:

乐观锁:会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas

乐观锁的典型代表:就是cas,利用cas进行无锁化机制加锁,var5 是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值

其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。

修改代码方案

我们的乐观锁保证stock大于0 即可,如果查询逻辑stock不能保证大于0,则会出现 success为false我们在后文进行判断即可。

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
   if (!success) {
        //扣减库存
        return Result.fail("库存不足!");
    }

代码写到这里,我们就解决了多线程安全问题(优惠券超卖)

一人一单

但是我们在检查数据库数据时,我们发现一个人可以购买多个优惠券。

因此我们可以在抢购前,判断该用户是否已经购买过该优惠券,如果购买过则直接返回。

【逻辑图】红框内的是新增逻辑。

 @Override
public Result seckillVoucher(Long voucherId) {
    // 1.查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 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.一人一单逻辑
    // 5.1.用户id
    Long userId = UserHolder.getUser().getId();
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    // 5.2.判断是否存在
    if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }

    //6,扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update();
    if (!success) {
        //扣减库存
        return Result.fail("库存不足!");
    }
    //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);

    return Result.ok(orderId);

}

【分析代码】---仍然会出现多线程问题。

        存在问题:现在的问题还是和之前一样,并发过来,查询数据库,都不存在订单,所以我们还是需要加锁,但是乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作

【注意事项】

  • 事务应该包含在锁的内部。
  • 锁的粒度,锁的对象应该是用户级别的,而不是整个抢购优惠券级别的,因此我们不会直接将synchronized加到方法上。
  • 锁对象的细节处理,使用userId.toString().intern()保证对象唯一。
  • 获取代理对象调用切入事务
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.RedisWorker;
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>
 *
 * @author msf
 * @since 2022-10-29
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisWorker redisWorker;

    @Override

    public Result seckillVoucher(Long voucherId) {
        // 1. 查询优惠券信息
        SeckillVoucher voucherOrder = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucherOrder.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("抢购尚未开始");
        }
        if (voucherOrder.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("抢购已经结束");
        }
        // 3.判断库存是否充足
        if (voucherOrder.getStock() < 1) {
            return Result.fail("您来晚了,票已被抢完");
        }
        Long userId = UserHolder.getUser().getId();
        // 事务应该在synchronized里面
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId,userId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId,Long userId) {
            // 4. 一人一单逻辑
            // 4.1 根据优惠券id和用户id查询订单
            Integer count = query().eq("user_id", userId)
                    .eq("voucher_id", voucherId).count();
            // 4.2 订单存在,直接返回
            if (count > 0) {
                return Result.fail("用户已经购买一次");
            }

            // 5. 扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .gt("stock", 0)
                    .eq("voucher_id", voucherId).update();
            if (!success) {
                return Result.fail("库存不足");
            }

            // 6.创建订单
            VoucherOrder order = new VoucherOrder();
            // 6.1 设置id
            order.setId(redisWorker.nextId("order"));
            // 6.2 设置订单id
            order.setVoucherId(voucherId);
            // 6.3 设置用户id
            order.setUserId(userId);
            save(order);

            // 7. 返回订单id
            return Result.ok(order);

    }
}

展望

虽然我们利用锁和事务解决单体系统下的秒杀功能,但是现在的业务一般是在集群和分布式系统协作完成,因此我们在测试系统在集群部署时,仍会出现一人多单问题,稍后我们将更新文章,分析问题出现原因,并利用分布式锁的方式解决该问题。

到此这篇关于Redis解决优惠券秒杀的文章就介绍到这了,更多相关Redis优惠券秒杀内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis分布式锁解决秒杀超卖问题

    目录 分布式锁应用场景 单体锁的分类 分布式锁核心逻辑 分布式锁实现的问题——死锁和解决 Redis解决删除别人锁的问题 分布式锁应用场景 秒杀环境下:订单服务从库存中心拿到库存数,如果库存总数大于0,则进行库存扣减,并创建订单订单服务负责创建订单库存服务负责扣减库存 模拟用户访问库存 多线程并发访问,出现超卖问题,线程不安全.没有保证原子性 单体锁的分类 单体应用锁指的是只能在 一个JVM 进程内有效的锁.我们把这种锁叫做单体应用锁 synchronized锁ReentrantLock锁一个

  • Redis优惠券秒杀解决方案

    目录 1 实现优惠券秒杀功能 2 超卖问题(重点) 1.版本号法 2.CAS法 1 实现优惠券秒杀功能 下单时需要判断两点:1.秒杀是否开始或者结束2.库存是否充足 所以,我们的业务逻辑如下 1. 通过优惠券id获取优惠券信息 2.判断秒杀是否开始,如果未返回错误信息 3.判断秒杀是否结束,如果已经结束返回错误信息 4.如果在秒杀时间内,判断库存是否充足 5.如果充足,扣减库存 6.创建订单信息,并保存到优惠券订单表中 6.1 保存订单id 6.2保存用户id 6.3保存优惠券id 7.返回订单

  • Redis优惠券秒杀企业实战

    目录 一.全局唯一ID 1. 全局ID生成器 2. 全局唯一ID生成策略 3. Redis自增ID策略 二.实现优惠券秒杀下单 1. 添加优惠券 2. 编写添加秒杀券的接口 三.实现秒杀下单 四.超卖问题 1. 加锁方式 - 乐观锁 2. 乐观锁解决超卖问题 3. 小结 五.一人一单问题 1. 加锁分析 2. 事务分析 六.集群模式下并发安全问题 一.全局唯一ID 1. 全局ID生成器 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单

  • Redis实现商品秒杀功能页面流程

    目录 全局唯一ID 业务逻辑分析 代码实现 优惠券秒杀 业务逻辑分析 代码实现 定量商品多卖问题 业务逻辑分析 乐观锁与悲观锁 乐观锁代码实现 一个用户限买一单 业务逻辑分析 代码实现 全局唯一ID 业务逻辑分析   全局唯一ID是针对销量比较大的一些商品而言的,这类商品的成交量比较多,用户购买成功就会生成对应订单信息并保存到一张表中,而订单表的id如果使用数据库自增ID就存在一些问题,比如说id的规律性太强导致安全性极低,还有如果订单数量太多一张表存不下分成多张表存储的话就会出现ID冲突问题,

  • Redis中的String类型及使用Redis解决订单秒杀超卖问题

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题. Redis中5种数据结构之String类型:key-value的缓存,支持过期,value不超过512M. Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,这些看上去像是组合命

  • Redis实现优惠券限一单限制详解

    需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单 我们只需要在增加订单之前,拿用户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.Voucher

  • Redis解决库存超卖问题实例讲解

    商品和订单服务间使用MQ 商品服务的库存变化时,通过 MQ 通知订单服务库存变化. 原始的同步流程 查询商品信息 (调用商品服务) 计算总价(生成订单详情) 商品服务扣库存(调用商品服务) 订单入库( 生成订单) // 原始的MySQL同步流程 // 判断此代金券是否加入抢购 SeckillVouchers seckillVouchers = seckillVouchersMapper.selectVoucher(voucherId); AssertUtil.isTrue(seckillVouc

  • JavaScript仿京东实现秒杀倒计时案例详解

    功能介绍: 1.这个倒计时是不断变化的,因此需要定时器来自动变化(setInterval) 2.三个黑色的盒子,分别存放时.分秒 3.三个盒子利用innerHTML存入倒计时 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content=&quo

  • Springboot+redis+Vue实现秒杀的项目实践

    目录 1.Redis简介 2.实现代码 3.启动步骤 4.使用ab进行并发测试 5.线程安全 6.总结 7.参考资料 1.Redis简介 Redis是一个开源的key-value存储系统. Redis的五种基本类型:String(字符串),list(链表),set(集合),zset(有序集合),hash,stream(Redis5.0后的新数据结构) 这些数据类型都支持push/pop.add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的. Redis的应用场景为配合关

  • redis使用watch秒杀抢购实现思路

    本文实例为大家分享了redis使用watch秒杀抢购的具体代码,供大家参考,具体内容如下 1.使用watch,采用乐观锁 2.不使用悲观锁,因为等待时间非常长,响应慢 3.不使用队列,因为并发量会让队列内存瞬间升高 代码: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import redis.clients.jedis.Jedis; /** * redis测试抢购 * *

  • php+redis实现商城秒杀功能

    好久没来整理文章了,闲了没事写篇文章记录下php+redis实现商城秒杀功能. 1.安装redis,根据自己的php版本安装对应的redis扩展(此步骤简单的描述一下) 1.1.安装php_igbinary.dll,php_redis.dll扩展此处需要注意你的php版本如图: 1.2.php.ini文件新增extension=php_igbinary.dll;extension=php_redis.dll两处扩展 ok此处已经完成第一步redis环境搭建完成看看phpinfo 2.项目中实际使

  • springboot集成redis实现简单秒杀系统

    本文实例为大家分享了springboot集成redis实现简单秒杀系统的具体代码,供大家参考,具体内容如下 项目是有地址的,我会放到文章的最后面 1. 直接service,我们会介绍两种秒杀模式 public interface GoodsService { /** * 通过lua脚本实现的秒杀 * @param skuCode 商品编码 * @param buyNum 购买数量 * @return 购买数量 */ Long flashSellByLuaScript(String skuCode

  • redis 解决key的乱码问题,并清理详解

    key乱码问题 因redis默认使用JdkSerializationRedisSerializer来进行序列化,造成key是乱码,如下: keys '*!report:flag:phon*' 1) "\xac\xed\x00\x05t\x00!report:flag:phone_156464" 2) "\xac\xed\x00\x05t\x00!report:flag:phone_198946" 3) "\xac\xed\x00\x05t\x00!repo

随机推荐