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.返回订单id

代码实现:(Service层实现类)

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 javax.annotation.Resource;
import java.time.LocalDateTime;
/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@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)
                .update();
        if (!success){
            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);
        //7.返回订单id
        return Result.ok(orderId);
    }
}

2 超卖问题(重点)

我们先尝试在高并发的情况下运行上述代码。(使用jmx工具)

下图是创建了两百个线程,在一瞬间发出优惠券请求

但是我们看聚合报告,发现异常值只有45.5%,按道理来说应该是50%(因为库存有100个,这里发出了200个请求)

一看库存数,好家伙,是-9

订单也是添加了109个,这显然发生了超卖的问题。

那么,为什么会发生这种问题呢?

看图说话:

按照我们正常的流程来走,就是线程1线查询完库存,然后扣减库存,这个时候线程2再来查询库存,扣减库存,这样是没问题的。

超卖的问题就出在,在订单1查询库存后,发现是1,但还没去扣减的时候,线程2也来查询库存,发现也是1,也进行了扣减(高并发的场景下)

这就导致了超卖的问题。

对于这种高并发的问题,最常见的解决方法就是:上锁~

但锁又包括悲观锁和乐观锁。

悲观锁简单的讲就是:觉得线程一定会发生,然后在操作之前每个人先拿锁,你执行完后,在轮到下一个来执行(串行执行)

乐观锁 :就是乐观(认为线程安全一定不会发生),只要在每次对数据修改之前,判断其他线程是否对数据进行的修改来保证线程安全。

悲观锁较为简单,这里实现乐观锁。

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种

温馨提示:左边表格的数据都是线程1执行后的数据哦~

1.版本号法

就是在查询库存的步骤上加上一个版本号,每次修改完数据后给版本号+1并在后面加上where条件判断版本号是否和修改前的一致

这样就可以做到线程安全啦~

2.CAS法

这个就是不用版本号了,直接在修改数据库后加上where条件判断库存是否是修改前的库存

解决超卖问题代码实现:

说到底就是在我们扣减库存的时候加上一个where条件判断库存是否大于0

//5.1扣减库存
boolean success = iSeckillVoucherService.update()
.setSql("stock = stock-1").eq("voucher_id" , voucherId).gt("stock" ,0)
.update();
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 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("库存不充足!");
        }
        //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);
    }
}

超卖问题解决

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

(0)

相关推荐

  • Redis优惠券秒杀企业实战

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

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

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

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

    目录 [前端页面] [分析代码] 一人一单 展望 虽然本文是针对黑马点评的优惠券秒杀业务的实现,但是是适用于各种抢购活动,保证线程安全. 摘要:本文先讲了抢购问题,指出其中会出现的多线程问题,提出解决方案采用悲观锁和乐观锁两种方式进行实现,然后发现在抢购过程中容易出现一人多单现象,为保证优惠券不会被[黄牛]抢到,因此我们在保证多线程安全的情况下实现了一人一单业务,最后指出本文的实现在集群情况下的不足之处.在本专栏的另一篇文章中提出集群或者分布式系统的解决方案. [前端页面] 在代金券发放后,多个

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

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

  • Redis中秒杀场景下超时与超卖问题的解决方案

    目录 超时 1.redis连接超时原因 2.解决方法 超卖 1.秒杀超卖现象 2.解决方案 (1)利用乐观锁淘汰用户,解决超卖问题 (2).使用reids的 watch + multi + setnx 指令实现 在开发过程中高并发问题是很棘手的一个问题(对于博主这样的小菜鸡来说),当我们学习redis之前,知道redis是单线程运行的所以任务不会出现线程不安全问题.当我们在linux中使用ab来模拟高并发秒杀时可能会遇到两种问题,“超时和超卖”. 超时 1.redis连接超时原因 (1)虚拟机中

  • php和redis实现秒杀活动的流程

    1 说明 前段时间面试的时候,一直被问到如何设计一个秒杀活动,但是无奈没有此方面的实际经验,所以只好凭着自己的理解和一些资料去设计这么一个程序 主要利用到了redis的string和set,string主要是利用它的k-v结构去对库存进行处理,也可以用list的数据结构来处理商品的库存,set则用来确保用户进行重复的提交 其中我们最主要解决的问题是 -防止并发产生超抢/超卖 2 流程设计 3 代码 3.1 服务端代码 class MiaoSha{ const MSG_REPEAT_USER =

  • 使用Redis实现秒杀功能的简单方法

    1. 怎样预防数据库超售现象 设置数据库事务的隔离级别为Serializable(不可用) Serializable就是让数据库去串行化的去执行事务,一个事务执行完才能去执行下一个事务,效率太慢 在数据表上设置乐观锁字段,例如设置版本号(version) 不同事务在执行更新操作时,需要先判断一下版本号是否已被修改 代码实现乐观锁流程 1.1. 什么表需要设置乐观锁 出现同时修改同一条记录的业务,相应的数据表要设置乐观锁 不会出现同时修改同一记录的数据库,就不需要设置乐观锁 2. 利用Redis防

  • 详解SpringBoot2.0的@Cacheable(Redis)缓存失效时间解决方案

    问题   @Cacheable注解不支持配置过期时间,所有需要通过配置CacheManneg来配置默认的过期时间和针对每个类或者是方法进行缓存失效时间配置. 解决   可以采用如下的配置信息来解决的设置失效时间问题 配置信息 @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { return new RedisCacheManager( RedisCacheWriter.no

  • 浅谈Redis缓存雪崩解决方案

    目录 1.保持缓存层的高可用 2.限流降级组件 3.缓存不过期 4.优化缓存过期时间 5.使用互斥锁重建缓存 6.异步重建缓存 缓存层承载着大量的请求,有效保护了存储层.但是如果由于大量缓存失效或者缓存整体不能提供服务,导致大量的请求到达存储层,会使存储层负载增加(大量的请求查询数据库) .这就是缓存雪崩的场景; 解决缓存雪崩可以从下面的几点着手: 1.保持缓存层的高可用 使用Redis哨兵模式或者Redis集群部署方式,即是个别Redis节点下线,整个缓存层依然可以使用.除此之外还可以在多个机

  • springboot +rabbitmq+redis实现秒杀示例

    目录 实现说明 1.工具准备 2.数据表 3.pom 4.代码结构 5.配置config 6.订单业务层 7.redis实现层 8.mq实现层 9.redis模拟初始化库存量 10.controller控制层 11.测试 12.测试结果 实现说明 这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库.如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的.因此,我们能做的只能是减少对数据库的访问.例如,前端发出了100万个请求,

  • redis分布式ID解决方案示例详解

    目录 常用的分布式ID解决方案 UUID Snowflake Snowflake算法的Java代码: Leaf Leaf算法的Java代码: 基于数据库自增ID生成 基于UUID生成 基于Redis生成 基于ZooKeeper生成 常用的分布式ID解决方案 在分布式系统中,生成全局唯一ID是非常重要的,因为在分布式系统中,多个节点同时生成ID可能会导致ID冲突. 下面介绍几种常用的分布式ID解决方案. UUID UUID(通用唯一标识符)是由128位数字组成的标识符,它可以保证在全球范围内的唯一

  • Java使用Redis实现秒杀功能

    秒杀功能 秒杀场景现在已经非常常见了,各种电商平台都有秒杀的产品,接下来我们模拟一个秒杀的项目,最终能够确保高并发下的线程安全.界面比较简单,但是功能基本实现. 界面 点击"秒杀点我"按钮后台就会输出秒杀结果. 第一版 使用Redis缓存数据库,使用一个key-value存储秒杀商品数量,使用set集合存储秒杀成功的用户.我们以商品0101为示例,设置商品的初始数量为200件.不考虑并发问题,实现功能. html.jsp.servlet文件不重要省略. package com.redi

  • 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

随机推荐