Redis优惠券秒杀 | 黑马点评

news2024/11/17 20:50:18

目录

一、全局唯一ID

1、全局ID生成器

二、实现秒杀下单

1、基本的下单功能

2、超卖问题

3、乐观锁解决并发问题

三、实现一人一单

1、思路分析

2、代码初步实现 

3、关于锁的范围 

4、关于事务失效

5、集群下线程并发问题


一、全局唯一ID

订单如果用自增长会存在的问题:

ID的规律性太明显了

受单表数量限制,因为如果商城很大订单表数量可能很多,要分库分表,到时候id自增从1开始的话肯定会出现重复的。订单表为了后边方便查询肯定不能重复

1、全局ID生成器

全局id生成器,是一种分布式系统下用来生成全局唯一ID的工具,满足下列特征:

  • 唯一性
  • 高可用
  • 高性能(生成足够快)
  • 递增性(整体递增,方便创建索引)
  • 安全性(规律性不能太明显)

Redis肯定唯一的,性能也高,Redis也是采用递增方案的

生成器代码(Redis自增ID策略):

在最后做拼接的时候,我们不能直接拼接,因为是long类型来接收所以我们得用位运算,前面的左移动32位然后或运算后面的

key的设置是每天一个key,方便订单统计也防止可能会重复

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {

    private static final long BEGIN_TIMESTAMP = 1640995200L;

    private static int COUNT_BITS = 32;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public Long nextId(String keyPrefix){
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long time = nowSecond - BEGIN_TIMESTAMP;

        String format = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // Redis Incrby 命令将 key 中储存的数字加上指定的增量值。
        // 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + format);

        return time << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime of = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long l = of.toEpochSecond(ZoneOffset.UTC);
        // LocalTime类的toEpochSecond()方法用于
        // 将此LocalTime转换为自1970-01-01T00:00:00Z以来的秒数
        System.out.println(l);
    }
}

二、实现秒杀下单

1、基本的下单功能

下单时需要满足两点:

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

实现代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        // 判断秒杀是否还未开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            Result.fail("秒杀尚未开始!");
        }

        // 判断秒杀是否已经结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            Result.fail("秒杀已经结束!");
        }

        // 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            Result.fail("库存不足!");
        }

        // 扣减库存
        boolean success = seckillVoucherService.update().
                setSql("stock = stock - 1").
                eq("voucher_id", voucherId).update();

        // 扣减失败
        if(!success){
            return Result.fail("库存不足!");
        }

        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 生成订单 id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setVoucherId(voucherId);
        // 用户 id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        voucherOrder.setId(orderId);
        save(voucherOrder);

        return Result.ok(orderId);
    }
}

记得方法加上事务注解,一旦出问题可以回滚。

2、超卖问题

当线程1在查到还有1个库存,然后开始扣除的时候,在还没扣除完毕时,这个时候有其他线程看到还有1个库存,都会进行扣除,这种情况就会存在超卖问题了。 

针对这一问题常见解决方案就是加锁,常见有乐观锁悲观锁

乐观锁

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

版本号法(数据库多一个version来标记是否已经修改)

CAS法(除了多的字段,版本号信息,以库存信息本身有没有变化为判断依据,当线程修改库存时,当线程修改库存时,判断当前数据库中的库存与之前查询得到的库存数据是否一致,如果一致,则说明线程安全,可以执行扣减操作,如果不一致,则说明线程不安全,扣减失败。)

3、乐观锁解决并发问题

我们只需要在修改库存表前判断一下,跟之前查到的值是否相等就行

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        // 判断秒杀是否还未开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            Result.fail("秒杀尚未开始!");
        }

        // 判断秒杀是否已经结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            Result.fail("秒杀已经结束!");
        }

        // 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            Result.fail("库存不足!");
        }

        // 扣减库存
        boolean success = seckillVoucherService.update().
                setSql("stock = stock - 1").
                eq("voucher_id", voucherId).
                eq("stock", seckillVoucher.getStock()).    // 增加对库存的判断,判断当前库存是否与查询出的结果一致
                update();

        // 扣减失败
        if(!success){
            return Result.fail("库存不足!");
        }

        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 生成订单 id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setVoucherId(voucherId);
        // 用户 id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        voucherOrder.setId(orderId);
        save(voucherOrder);

        return Result.ok(orderId);
    }
}

最后我们测试居然发现原本预测执行100条订单的,但是实际上只有76条,为什么呢?

因为我们这种设置乐观锁太保守了,只要查到库存与之前不一样就不能扣除库存,但是实际上在库存还有很多的时候,这种是不影响的还是可以扣除的。于是我们优化:

// 扣减库存
        boolean success = seckillVoucherService.update().
                setSql("stock = stock - 1").
                eq("voucher_id", voucherId).
                // 增加对库存的判断,判断当前库存是否与查询出的结果一致
                // eq("stock", seckillVoucher.getStock()).    
                // 修改判断逻辑,改为只要库存大于0,就允许线程扣减
                gt("stock", 0).        
                update();

只要库存还是大于0的就能够进行修改

三、实现一人一单

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

1、思路分析

我们得从查询订到到判断订单到创建订单这三步都要加上悲观锁,我们是同一个用户来了才需要处理这个并发安全问题,不同的用户是不影响的,因此加的锁应该根据用户的id来加锁

所以用synchronize(userId.toString().intern())这个来锁,为什么要加intern(),因为如果不加每次获取的字符串对象可能不是一个都是不一样的,加了可以保证每次都是同一个,他会去常量池里面找一样的返回。

2、代码初步实现 

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {

        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        // 判断秒杀是否还未开始
        if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            Result.fail("秒杀尚未开始!");
        }

        // 判断秒杀是否已经结束
        if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            Result.fail("秒杀已经结束!");
        }

        // 判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            Result.fail("库存不足!");
        }

        return createVoucherOrder(voucherId);
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 判断当前优惠券用户是否已经下过单
        // 用户 id
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            if (count > 0) {
                return Result.fail("用户已经购买过一次");
            }

            // 扣减库存
            boolean success = seckillVoucherService.update().
                    setSql("stock = stock - 1").
                    eq("voucher_id", voucherId).
//                eq("stock", seckillVoucher.getStock()).    // 增加对库存的判断,判断当前库存是否与查询出的结果一致
        gt("stock", 0).        // 修改判断逻辑,改为只要库存大于0,就允许线程扣减
                    update();

            // 扣减失败
            if (!success) {
                return Result.fail("库存不足!");
            }

            // 创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 生成订单 id
            Long orderId = redisIdWorker.nextId("order");
            voucherOrder.setVoucherId(voucherId);

            voucherOrder.setUserId(userId);
            voucherOrder.setId(orderId);
            save(voucherOrder);

            return Result.ok(orderId);
        }
    }
}

3、关于锁的范围 

这样加也有弊端,如果锁的范围是这里,锁先释放再提交的事务,假如我们刚改完释放锁还没提交事务,别人进来又改一次,然后再提交事务就会出现问题。

我们必须把锁加在外面,调用方法的时候锁住,锁住整个方法,事务先提交再释放锁

synchronize(userId.toString().intern()){
    return createVoucherOrder(voucherId);
}

4、关于事务失效

这样做会导致事务失效,我们现在给的是方法加的事务注解,seckillVoucher这个方法没有加,现在本质上是用this.createVoucherOrder来调用的,这个this拿到的是当前对象来调用的,而不是代理对象调用。

我们要想让事务生效,是spring对当前类做了动态代理,生成代理类,用代理对象来做的事务处理。现在用的是非代理对象来做的,所有没有事务功能。

我们要拿到事务代理对象才行。

我们可以用AopContext拿到代理对象,然后用代理对象来调用方法。

这样做我们要添加一个aspectjweaver的依赖,启动类添加@EnableAspectJAutoProxy(exposeProxy=true)注解来暴露代理对象

5、集群下线程并发问题

上面这种情况下只能保证单机部署下安全,在集群环境还是会出现问题

我们模拟集群的环境:

 测试发现集群模式下synchronize锁不住,为什么呢?

在集群模式下,每个都是不同tomcat,不同jvm的存在,每个jvm的每个锁都可以有一个线程来获取,就会出现并行安全问题。

要想解决这种问题,必须得想办法让多个jvm只能用同一个锁。分布式锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/174383.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT动画实例代码QPropertyAnimation的应用

用QT实现动画&#xff0c;我们必定用到QPropertyAnimation&#xff0c;这里我们介绍几种情形的动画实现。如直线动画&#xff0c;曲线动画&#xff0c;路径动画。 一、基础知识 1、QPropertyAnimation的初始化 我们首先必须在包涵QPropertyAnimation的头文件或者模块&#x…

Android之WorkManager处理后台定时任务

WorkManager和Service并不相同&#xff0c;也没有直接的联系。Service是Android系统四大组件之一&#xff0c;它没有被销毁的情况下是一直保持在后台运行的。而WorkManager只是一个处理定时任务的工具&#xff0c;它可以保证即使在应用退出甚至手机重启的情况下&#xff0c;之前…

动手深度学习-pytorch数据操作

N维数组是机器学习和神经网络的主要数据结构创建数组需要形状&#xff1a;如3*4的矩阵每个元素的类型&#xff1a;例如32位浮点数每个元素的值&#xff1a;例如全是0.或者随机数数据操作首先&#xff0c;导入torch张量表示一个数值组成的数组&#xff0c;这个数组可能有多个维度…

acwing基础课——欧拉函数

由数据范围反推算法复杂度以及算法内容 - AcWing 常用代码模板4——数学知识 - AcWing 基本思想&#xff1a; 这里我们了解一下欧拉函数是什么以及用筛法求欧拉函数&#xff0c;我们先给出欧拉函数的定义&#xff1a; 然后我们了解一下互质的概念&#xff0c;只要两数的公因…

连你女朋友都能看懂的分布式架构原理!

目录 从一个新闻门户网站案例引入推算一下你需要分析多少条数据&#xff1f;黄金搭档&#xff1a;分布式存储分布式计算 这篇文章聊一个话题&#xff1a;什么是分布式计算系统&#xff1f; 一、从一个新闻门户网站案例引入 现在很多同学经常会看到一些名词&#xff0c;比如分…

CSDN官方模板

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

数据结构(栈)

目录 栈的定义 形象比喻 栈的相关术语 栈的抽象数据类型&#xff08;栈Stack的ADT&#xff09; 顺序栈 顺序栈类的声明 顺序栈类成员函数的实现 基本效率分析 顺序栈的应用&#xff08;小测试&#xff09; main.cpp 共享栈 双共享栈 链式栈 链式栈基本操作分析 链…

cf Educational Codeforces Round 134 E. Prefix Function Queries

原题&#xff1a; You are given a string s, consisting of lowercase Latin letters. You are asked q queries about it: given another string t, consisting of lowercase Latin letters, perform the following steps: concatenate s and t; calculate the prefix func…

实战打靶集锦-001-Funbox2

**写在前面&#xff1a;**这应该是本人第一次自主成功完成的打靶&#xff0c;纪念一下下。 目录1. 主机发现2. 端口扫描3. 服务枚举4. 服务探查4.1 Apache探查4.1.1 浏览器访问4.1.2 站点地图查看4.1.3 目录枚举4.1.4 公共EXP搜索4.2 FTP探查4.2.1 手工登录FTP4.2.2 公共EXP搜索…

基于决策树模型和支持向量机模型的手写数字识别

目录 1、导入库和手写数字数据集 2、 把数据可视化 3、把数据分成训练数据集和测试数据集 4、训练SVM模型 5、训练决策树模型 6、对所使用的模型进行评估 7、对手写数字图像进行预测 本项目实现了 第一个功能&#xff1a;可以通过导入库和数据集、通过对数据集的预处理…

JavaScript基础(17)_Function方法(call、apply)、arguments

概念 call&#xff0c;apply都属于Function.prototype的一个方法&#xff0c;它是JavaScript引擎内在实现的&#xff0c;因为属于Function.prototype&#xff0c;所以每个Function对象实例(就是每个方法)都有call&#xff0c;apply属性&#xff0c;可以通过函数对象来调用。 a…

2023牛客寒假算法基础集训营3 赛时思路+正解

这场数学和思维偏多&#xff0c;特别是数学&#xff0c;五个小时过于充实了&#xff0c;而且更加考验你的心态。 这场不乏码量大的毒瘤题&#xff0c;也不乏人类智慧妙妙题。 A 不断减损的时间 题意 给定一个数组aaa&#xff0c;我们可以执行任意次操作&#xff0c;该操作定义…

Rethinking Performance Gains in Image Dehazing Networks

论文源码&#xff1a;https://download.csdn.net/download/zhouaho2010/87393184 Abstract 图像去雾是低层视觉中的一个活跃话题&#xff0c;随着深度学习的快速发展&#xff0c;许多图像去雾网络被提出。尽管这些网络的工作良好&#xff0c;但提高图像去雾性能的关键机制仍不…

守望者的逃离

题目说明【问题描述】恶魔猎手尤迫安野心勃勃.他背叛了暗夜精灵&#xff0c;率深藏在海底的那加企图叛变&#xff1a;守望者在与尤迪安的交锋中遭遇了围杀.被困在一个荒芜的大岛上。为了杀死守望者&#xff0c;尤迪安开始对这个荒岛施咒&#xff0c;这座岛很快就会沉下去&#…

【java入门系列五】java基础-面向对象**

学习记录&#x1f914;类与对象内存中的布局堆栈成员方法 类内的函数传参机制传进来的是一个数组/对象&#xff0c;类似于py的list调用递归-在栈空间方法递归汉诺塔八皇后方法重载overload可变参数作用域**scope构造器/构造方法&#xff1a;新对象的初始化构造器细节对象创建流…

pytorch 笔记:torch.nn.init

这个模块中的所有函数都是用来初始化神经网络参数的&#xff0c;所以它们都在torch.no_grad()模式下运行&#xff0c;不会被autograd所考虑。 1 计算gain value 1.1 介绍 这个在后面的一些nn.init初始化中会用到 1.2 用法 torch.nn.init.calculate_gain(nonlinearity, para…

【LeetCode】回溯算法总结

回溯法解决的问题 回溯法模板 返回值&#xff1a;一般为void参数&#xff1a;先写逻辑&#xff0c;用到啥参数&#xff0c;再填啥参数终止条件&#xff1a;到达叶子节点&#xff0c;保存当前结果&#xff0c;返回遍历过程&#xff1a;回溯法一般在集合中递归搜索&#xff0c;集…

使用DiskGenius进行硬盘数据迁移

克隆硬盘 - DiskGenius 1.迁移磁盘 选择自己想要迁移的磁盘&#xff0c;点击工具-克隆磁盘 首先选择源硬盘&#xff0c;点击确定 之后选择想要迁移到的硬盘&#xff0c;点击确定 检查一下原硬盘和目标硬盘是否正确&#xff0c;此外还可以对这个空间进行二次调整。最终如果没有…

Android 中关于 FileObserver类监听文件状态的实践

文章目录需求背景走进源码实现示例参考需求背景 当某一个目录的文件发生变化&#xff08;创建、修改、删除、移动&#xff09;时&#xff0c;需要给一个回调事件给其他端调用。 其他场景&#xff1a;阅后即焚等等。 比如在 Android 的 VR 设备中&#xff0c;有一个用于部署的文…

Oracle P6 Professional专业版 22.12 中的热门新功能

目录 并排查看项目 在复制与 WBS 元素的关系时具有更大的灵活性 更轻松地确定要分配的正确基线 复制并粘贴电子表格中的单元格区域 更好地控制导入数据 检查 P6 专业版中提供的时间表报告 在排序对话框中排列字段顺序 创建导入和导出模板的副本 指定完成日期筛选器如何…