首页 > 数据库 >基于Redis实现的优惠券秒杀业务

基于Redis实现的优惠券秒杀业务

时间:2022-10-18 20:55:29浏览次数:51  
标签:优惠券 Redis voucherId 秒杀 voucher Result id stock

全局唯一Id生成器

为实现唯一性、递增性、安全性、高可用、高性能,能支持未来大量订单业务的订单id的快速生成,给出一种id生成的方法。

代码实现

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**
 * @Description TODO
 * @Author ygw
 * @Date 2022/10/18 9:13
 * @Version 1.0
 */

@Component
public class RedisIdWorker {
    /**
     * 开始的时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    public long nextId(String key){
        //1、生成时间戳
        LocalDateTime time = LocalDateTime.now();
        long nowTimestamp = time.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowTimestamp - BEGIN_TIMESTAMP;

        //2、生成序列号,由于给递增序列号设置为32位,需要在生成序列号时给出限制
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy:MM:dd");
        String date = time.format(timeFormatter);

        /**
         * 重点关注此处redis数据结构的设置,inc:deal:20221018 - value,也就是说一天允许生成2^32个订单
         */
        Long count = stringRedisTemplate.opsForValue().increment("inc:" + key + ":" + date);

        //3、合并生成id
        long id = (timestamp << 32) | count;
        return id;
    }

}

秒杀业务流程

业务流程

说明

1、抢购前,需要判断当前时间活动是否开始或结束

2、如果当前为活动进行时间段,对库存进行判断,存量减一(需要考虑并发)

3、抢到了消费券之后,就直接创建对应的id

注意

需要开启事务,Spring默认使用数据库的隔离级别,mysql的默认隔离级别为可重复读(Repeated Read)(可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读),也就是说不用考虑脏读、不可重复读的问题。

代码实现

SeckillVoucherServiceImpl.seckillVoucher()

@Service
@Slf4j
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {

    @Resource
    private IVoucherOrderService voucherOrderService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        SeckillVoucher voucher = query().eq("voucher_id", voucherId).one();

        LocalDateTime now = LocalDateTime.now();

        //1、判断当前时间是否处在活动时间段内
        if (now.isBefore(voucher.getBeginTime()) || now.isAfter(voucher.getEndTime())) {
            //如果不再活动的时间段内,之间返回
            return Result.fail("请等待活动开放后重试");
        }

        //2、如果处在活动时间段内,对库存进行判断
        if(voucher.getStock() < 1){
            return Result.fail("优惠券已经抢完了");
        }

        //3、如果库存大于0,优惠券数量减一并创建订单

        boolean success = update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if(!success){
            return Result.fail("优惠券被抢完了");
        }

        //4、创建订单返回订单的id
        long orderId = redisIdWorker.nextId("order");
        Long userId = UserHolder.getUser().getId();

        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setUserId(userId);
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);

        voucherOrderService.save(voucherOrder);

        return Result.ok(orderId);
    }
}

乐观锁解决超卖问题(并发)

也可以利用串行化的事务隔离级别来解决超卖的问题,但是效率很低

超卖问题可以看成是一种略高于不可重复读的问题,需要在可重复读的事务隔离级别上加锁

问题分析

说明:版本号法就是在select的时候获取版本号,此时如果有多个线程获取到同样的数据;那么最先修改数据的线程在修改数据的同时修改版本号,其他线程在修改数据的时候应该先比较版本号;如果版本号发生改变则无法修改数据,事务回滚。

说明:由版本号法的图中可以发现,仅在版本号为1时进行修改;利用这个特性刚好可以卡住使用stock为1时的状态,即在修改时对stock的值进行判断

代码实现

根据以上分析,可以就用stock的状态来作为一个乐观锁,代码部分只改变更新时的操作

boolean success = update()
    				.setSql("stock = stock - 1")
    				.eq("voucher_id", voucherId)
    				.gt("stock", 0)  //增加比较条件,即stock > 0
    				.update();

悲观锁解决一人一单的问题(并发)

问题分析

说明

在基本秒杀业务的基础上,增加了一个判断优惠券订单是否存在的分支,如果该用户已经抢到了优惠券就直接返回

代码实现

SeckillVoucherServiceImpl.java

@Service
@Slf4j
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {

    @Resource
    private IVoucherOrderService voucherOrderService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        SeckillVoucher voucher = query().eq("voucher_id", voucherId).one();

        LocalDateTime now = LocalDateTime.now();

        //1、判断当前时间是否处在活动时间段内
        if (now.isBefore(voucher.getBeginTime()) || now.isAfter(voucher.getEndTime())) {
            //如果不再活动的时间段内,之间返回
            return Result.fail("请等待活动开放后重试");
        }

        //2、如果处在活动时间段内,对库存进行判断
        if(voucher.getStock() < 1){
            return Result.fail("优惠券已经抢完了");
        }

        //3、如果库存大于0,优惠券数量减一并创建订单
        Long userId = UserHolder.getUser().getId();

        //使用intern()是为了确保锁住的是toString后常量池中的值,而不是引用
        synchronized (userId.toString().intern()){
            /**
             * 关于事务失效的说明
             * 在spring中我们将SeckillVoucherServiceImpl交由proxy来进行代理
             * 也就是说SeckillVoucherServiceImpl中的事务实际上由proxy来完成
             * 直接调用createVoucherOrder()事务实际上调用的是SeckillVoucherServiceImpl.createVoucherOrder()
             * 会引起事务的失效,因此下面的操作是为了防止事务失效
             */
            //需要加上aspectjweaver依赖,并在启动程序上开启@EnableAspectJAutoProxy(exposeProxy = true)
            ISeckillVoucherService seckillVoucherService = (ISeckillVoucherService) AopContext.currentProxy();

            return seckillVoucherService.createVoucherOrder(voucherId);
        }
    }

    /**
     * 我所理解的此处的事务为:
     * 事务确保的要么都成功,要么都失败然后进行回滚->是对事务的完整性进行保证
     * 锁是锁住变量然后针对这个变量进行的一系列操作->是对并发安全进行保证
     * @param voucherId
     * @return
     */
    @Transactional
    public Result createVoucherOrder(Long voucherId){
        Long userId = UserHolder.getUser().getId();
        /**
         * TODO 需要判断该用户是否已经抢到了优惠券
         * 并发情况下,可能有多个线程同时进入查询,获得相同的数据,同时满足了更新条件
         * 因此我们在查询时,需要对用户的id进行加锁
         * 此种方法选用的是悲观锁,直接加synchronized即可
         */
        Integer count = voucherOrderService.query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if(count > 0){
            return Result.fail("优惠券每人限领一张");
        }

        boolean success = update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
        if(!success){
            return Result.fail("优惠券被抢完了");
        }

        //4、创建订单返回订单的id
        long orderId = redisIdWorker.nextId("order");

        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setUserId(userId);
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);

        voucherOrderService.save(voucherOrder);

        return Result.ok(orderId);
    }
}

标签:优惠券,Redis,voucherId,秒杀,voucher,Result,id,stock
From: https://www.cnblogs.com/Gw-CodingWorld/p/16804087.html

相关文章

  • MAC m1使用homebrew安装redis报错
    报错信息tar:Erroropeningarchive:Failedtoopen'/Users/peas/Library/Caches/Homebrew/downloads/df016fccee96887f4f24a989ec9b08c04eef867bfb15f67a6e5eee58f6ce......
  • 自定义RedisUtils个工具类
    网上有很多可以自行查询;1、狂神的RedisUtil点击查看代码//在我们真实的开发中,或者在公司,一般都可以看到一个公司自己封装的Utils工具~~@ComponentpublicclassRedis......
  • Windows中redis未授权通过dll劫持上线
    转自https://mp.weixin.qq.com/s/U-UD7h92fEJzCjyGbgF4Og前言项目中时不时会遇到windows的redis未授权,利用dll劫持可以不用重启获取shell。本文参考网上师傅们的文章详细......
  • ubuntu18安装redis后未开机启动Could not connect to Redis at 127.0.0.1:6379: Conne
    阿里云ubuntu18安装redis后,aptinstallredis-serverredis-cli提示CouldnotconnecttoRedisat127.0.0.1:6379:Connectionrefused最终发现是两方面导致:1.ubuntu18......
  • Redis6中的事务操作
    Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多......
  • Redis 发布订阅
    什么是发布和订阅在软件架构中,发布订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些......
  • Redis6 持久化
    RDB(RedisDataBase)在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里Redis会单独创建(fork)一个子进程来进......
  • centos7安装redis6.2
    一、下载安装包[root@glusterfs03~]#wgethttps://download.redis.io/releases/redis-6.2.7.tar.gz--2022-10-1810:35:21--https://download.redis.io/releas......
  • Redis
    非关系型数据库,结构上key-value存储。redisdoc.com#在线的命令手册memcache,是纯内存型数据库。redis是内存+磁盘。2者全部数据都是保存在内存中。redis3.0开始支持re......
  • Redis Zset的实现为什么用跳表,而不用平衡树?
    之前写过一篇Redis数据类型的底层数据结构的实现,其中提到,ZSet对象的底层数据结构实现之一是跳表。然后,有读者就问:为什么不使用平衡树(如红黑树、AVL树)?我们先来了解下......