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

目录
  • 全局唯一ID
    • 业务逻辑分析
    • 代码实现
  • 优惠券秒杀
    • 业务逻辑分析
    • 代码实现
  • 定量商品多卖问题
    • 业务逻辑分析
    • 乐观锁与悲观锁
    • 乐观锁代码实现
  • 一个用户限买一单
    • 业务逻辑分析
    • 代码实现

全局唯一ID

业务逻辑分析

  全局唯一ID是针对销量比较大的一些商品而言的,这类商品的成交量比较多,用户购买成功就会生成对应订单信息并保存到一张表中,而订单表的id如果使用数据库自增ID就存在一些问题,比如说id的规律性太强导致安全性极低,还有如果订单数量太多一张表存不下分成多张表存储的话就会出现ID冲突问题,于是我们需要一个全局ID生成器,保证ID在全局中都是唯一的

  使用Redis即可完成这种全局ID生成器的功能,具体实现就是一种类雪花算法,也就是符号位、时间戳、序列号三部分拼接形成一个ID,逻辑就是符号位0代表整数,时间戳确定具体到下订单的时候是哪一秒,至于序列号就是用于区分这一秒的订单,序列号使用redis的值自增来保证所有序列号不一致,原则上一秒中最多可以有232个不同的ID

代码实现

@Component
public class RedisIdGenerator {
    /**
     * 构造方法注入stringRedisTemplate对象
     */
    private StringRedisTemplate stringRedisTemplate;
    public RedisIdGenerator(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    // 定义序列号的位数
    private static final int COUNT_BITS = 30;
    public long nextId(String keyPrefix) {
        // 生成从指定时间到现在的时间戳
        LocalDateTime beginTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long beginTimeStamp = beginTime.toEpochSecond(ZoneOffset.UTC);
        long endTimeStamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
        long timeStamp = endTimeStamp - beginTimeStamp;
        /**
         * 生成序列号 使用redis的incr方法 K值为"icr:" + keyPrefix + ":" + date
         * 也就是按照日期作为K 每下一次单V就自增1作为序列号添加到后面
         * 这样的话既避免了K固定带来的V超过最大阈值(redis中的V最大为2^64)
         * 而且还方便了统计一天、一个月、一年的订单量,在这段时间内最大的序列号就是它的最多订单数
         */
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long sequenceId = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        // 拼接生成全局唯一ID并返回 两个二进制的拼接可以使用前一个数左移一定位数 后一个数与位移后的进行或运算
        return timeStamp << COUNT_BITS | sequenceId;
    }
}

优惠券秒杀

业务逻辑分析

  用户对秒杀商品下单的时候,后台业务需要先完成对商品时间的判断,判断该商品的秒杀活动是否开始或者有没有结束,但凡还未开始或者已经结束都无法下单;时间信息正确的话就判断该商品的活动库存还有没有剩余,如果已经卖完的话也无法下单。时间和库存的判断都是通过前端传过来的优惠券id,查出来该优惠券的时间和库存信息,如果条件都满足的话,将该商品券的库存扣除,然后创建订单返回订单id

代码实现

  controller层主要就是调用service接口里的secKillVoucher方法,所以整个业务逻辑代码全部都在接口的实现类中完成

@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdGenerator generator;
@Override
@Transactional
public Result secKillVoucher(Long voucherId) {
    // 查询优惠券
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    // 获取时间 判断秒杀活动是否开始或者结束
    if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("活动暂未开始");
    } else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("活动已经结束");
    }
    // 判断库存是否充足
    if (seckillVoucher.getStock() < 1) {
        return Result.fail("库存不足,活动结束");
    }
    // 扣减库存
    seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId).update();
    // 创建订单 并返回id
    VoucherOrder order = new VoucherOrder();
    // 订单id(redis全局唯一id) 下单用户id(拦截器中做登录验证的用户id) 优惠券id(直接传过来的id)
    long orderId = generator.nextId("order");
    order.setId(orderId);
    order.setUserId(UserHolder.getUser().getId());
    order.setVoucherId(voucherId);
    save(order);
    return Result.ok(orderId);
}

定量商品多卖问题

业务逻辑分析

  像上面的优惠券秒杀的业务,优惠券或者商品的数量一般都是固定的,如果把这些数量都卖完之后应该就结束这个活动。但是现实中的秒杀业务都是多线程的,很多的用户同时等着活动开启一起点击下单,这样的话就极有可能出现线程安全问题也就是说最终成交的数量要多于活动商品的数量

  上述问题出现的原因就是多线程之间的执行顺序所引起,我们的秒杀业务里面是先查询库存数量大于1就产生订单,但是多线程之间的执行不会严格的按照这个顺序执行,而是交叉执行,如果最后只剩一张票的时候进来了两个线程AB,A查完B查AB查询结果都可以下单,A产生订单B再产生订单,此时就已经产生超卖

乐观锁与悲观锁

  解决线程问题的最好方法就是加锁,但是锁也分为悲观锁和乐观锁,悲观锁认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行,例如Synchronized、Lock等。乐观锁认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改,如果没有修改则更新数据,修改说明发生了安全问题

  很显然乐观锁的性能要显著高于悲观锁,因此采用乐观锁保证线程的原子性。乐观锁又有两种解决方案:版本号是指对修改的数据附带一个version字段值,每次更新的时候判断修改时的version与查询的时候是否一致,一致则修改。CAS机制全称为Compare And Swap译为先比较再交换,也就是将修改的数据本身作为版本号,每次更新的时候判断修改时的数据值与查询时的值是否相同,相同则修改,不同就说明发生了线程安全问题,在我们的这个售卖业务中,可以设置成只要库存大于0就可以执行成功

乐观锁代码实现

  乐观锁的核心就是,在更新数据的时候(也就是减少库存),判断一下库存是否大于0,如果判断失败的话也应该使该线程任务失败

// 扣减库存
boolean update = seckillVoucherService.update()
        .setSql("stock = stock - 1")
        .eq("voucher_id", voucherId)
        .gt("stock", 0)
        .update();
// 更新失败说明在扣除库存的时候 库存小于等于0
if (!update) {
    return Result.fail("库存不足!");
}

一个用户限买一单

业务逻辑分析

  按照正常的业务逻辑,秒杀应该限制一个用户只能购买一次该商品,最简单的方法就是对user_id使用唯一索引,如果user_id重复就会抛出相关异常,但是这需要修改表结构。如果不修改标结果的话就需要扣除库存之前根据voucher_id和user_id查询订单表,如果存在的话就返回错误,否则说明该用户还未购买

代码实现

  单机(服务部署在一台tomcat服务器)的情况下,加synchronized 锁即可解决(查询判断用户是否下单和创建订单)业务的线程安全问题,但是这种情况就只能

// 单用户id(拦截器中做登录验证的用户id)
Long userId = UserHolder.getUser().getId();
// 根据user_id加锁  intern方法是去字符常量池中查找值相同的,不加的话字符串值一样的地址不一样也会加上锁
synchronized (userId.toString().intern()) {
	// 查询优惠券
	// 判断库存是否充足
    // user_id和voucher_id联合查询订单数
    Integer count = query().eq("user_id", userId)
            .eq("voucher_id", voucherId)
            .count();
    // 订单数为1 就说明已经下过单了
    if (count.equals(1)) {
        return Result.fail("您已经购买过该商品了");
    }
    // 扣减库存  创建订单
    return Result.ok(orderId);
}

  以上加synchronized 锁的解决方案只适用于单机模式下,此时所有的请求过来都会按照userId去常量池中查找是否一致,一致的话就锁在一起防止一个用户购买多单。但是集群模式下所有的请求会经过Nginx的负载均衡轮询发送到集群上的所有服务器,如果一个用户的多个请求被分配到不同的服务器上的话,不同服务器中的JVM虚拟机里的静态常量池中的内容是不同步的,这样的话就会导致虽然userId一致但是各自所在的静态常量池中都没有,于是这个用户就可以在不同的服务器分别下单了。如果有用户使用脚本同时发送很多的下单请求,那么就会有极大的可能在每一个服务器中都下一单,那么如何解决这个问题呢?那就要学习分布式锁的内容了

到此这篇关于Redis实现商品秒杀功能页面流程的文章就介绍到这了,更多相关Redis商品秒杀内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

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

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

  • Redis锁完美解决高并发秒杀问题

    目录 1 单机环境下的锁 2 分布式情况下使用Redis锁. 3 一台服务宕机,导致无法释放锁 4 给每一把锁加上过期时间 5延长锁的过期时间,解决锁失效 6 使用Redisson简化代码 场景:一家网上商城做商品限量秒杀. 1 单机环境下的锁 将商品的数量存到Redis中.每个用户抢购前都需要到Redis中查询商品数量(代替mysql数据库.不考虑事务),如果商品数量大于0,则证明商品有库存.然后我们在进行库存扣减和接下来的操作.因为多线程并发问题,我们不得不在get()方法内部使用同步代码块

  • Redis高并发场景下秒杀超卖解决方案(秒杀场景)

    目录 1 什么是秒杀 2 为什么要防止超卖 3 单体架构常规秒杀 3.1 常规减库存代码 3.2 模拟高并发 3.3 超卖现象 3.4 分析原因 4 简单实现悲观乐观锁解决单体架构超卖 4.1 悲观锁 4.2 乐观锁 4.3 redis锁setnx 4.4 使用Redision 5 分布式锁的解决方案 6 采用缓存队列防止超卖 1 什么是秒杀 秒杀最直观的定义:在高并发场景下而下单某一个商品,这个过程就叫秒杀 [秒杀场景] 火车票抢票 双十一限购商品 热度高的明星演唱会门票 … 2 为什么要防止

  • 秒杀场景的缓存、队列、锁使用Redis优化设计方案

    目录 一.为什么难 二.常见架构 三.优化方向 四.优化细节 五.Redis 六.总结 一.为什么难 秒杀系统难做的原因:库存只有一份,所有人会在集中的时间读和写这些数据.例如小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万.又例如12306抢票,亦与秒杀类似,瞬时流量更甚.这篇文章主要介绍了秒杀场景的缓存.队列.锁使用Redis优化设计方案,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 主要需要解决的问题有两个: 高并发对数据库产生

  • redis秒杀系统的实现

    目录 1.如何设计一个秒杀系统 2.秒杀流程 2.1 前端处理 2.2 后端处理 3.超卖问题 4.总体思路 1.如何设计一个秒杀系统 在设计任何系统之前,我们首先都需要先理解秒杀系统的业务背景 下面我简单的举一个例子: 在某个时间点,某某电商网站要低价卖某件商品,而且限量1千件,抢购人数超过数十万人.所以我们面临的第一个秒杀的问题就是:时间极短,然后瞬间流量非常大我们的系统必须保证秒杀抢购的结果不出错,达到抢购的预期目的.而且秒杀库存的实现也需要保障秒杀结果的准确性.总结几个特点就是: 高性能

  • Thinkphp5+Redis实现商品秒杀代码实例讲解

    环境:wamp,redis 要求:安装WAMP,Redis,以及为PHP安装Redis扩展 秒杀功能大致思路:获取缓存列表的长度,如果长度(llen)等于0,就停止秒杀,即秒杀失败,如果长度大于0,则继续运行,先从缓存中移除一个元素(lpop),再进行数据库操作(添加订单表,商品库存数量减一),如果再进一个人秒杀,就再走一遍流程,循环往复. 一.安装Redis扩展 1.查看PHP版本信息 打开phpinfo.php,查看PHP版本,我的是PHP7.3.4,还有一个需要注意Architecture

  • 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+RabbitMQ+Redis实现商品秒杀的示例代码

    目录 业务分析 创建表 功能实现 1.用户校验 2.下单 3.减少库存 4.支付 总结 业务分析 一般而言,商品秒杀大概可以拆分成以下几步: 用户校验 校验是否多次抢单,保证每个商品每个用户只能秒杀一次 下单 订单信息进入消息队列,等待消费 减少库存 消费订单消息,减少商品库存,增加订单记录 付款 十五分钟内完成支付,修改支付状态 创建表 goods_info 商品库存表 列 说明 id 主键(uuid) goods_name 商品名称 goods_stock 商品库存 package com.

  • 基于redis分布式锁实现秒杀功能

    最近在项目中遇到了类似"秒杀"的业务场景,在本篇博客中,我将用一个非常简单的demo,阐述实现所谓"秒杀"的基本思路. 业务场景 所谓秒杀,从业务角度看,是短时间内多个用户"争抢"资源,这里的资源在大部分秒杀场景里是商品:将业务抽象,技术角度看,秒杀就是多个线程对资源进行操作,所以实现秒杀,就必须控制线程对资源的争抢,既要保证高效并发,也要保证操作的正确. 一些可能的实现 刚才提到过,实现秒杀的关键点是控制线程对资源的争抢,根据基本的线程知识,可

  • 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实现高并发下的抢购、秒杀功能的实例

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个: 1 高并发对数据库产生的压力 2 竞争状态下如何解决库存的正确减少("超卖"问题) 对于第一个问题,已经很容易想到用缓存来处理抢购,避免直接操作数据库,例如使用Redis. 重点在于第二个问题 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数 <?php $conn=mysql_connect("localho

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

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

  • PHP商品秒杀问题解决方案实例详解【mysql与redis】

    本文实例讲述了PHP商品秒杀问题解决方案.分享给大家供大家参考,具体如下: 引言 假设num是存储在数据库中的字段,保存了被秒杀产品的剩余数量. if($num > 0){ //用户抢购成功,记录用户信息 $num--; } 假设在一个并发量较高的场景,数据库中num的值为1时,可能同时会有多个进程读取到num为1,程序判断符合条件,抢购成功,num减一.这样会导致商品超发的情况,本来只有10件可以抢购的商品,可能会有超过10个人抢到,此时num在抢购完成之后为负值. 解决该问题的方案由很多,可

  • Java使用Redis实现秒杀功能

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

随机推荐