【Redis】秒杀业务设计、悲观锁与乐观锁

news2024/11/14 20:35:34

1 全局ID生成器

一些情境下,使用数据库的ID自增将会产生一些问题。

  • 一方面,自增ID规律性明显,可能被猜测出来并产生一些漏洞
  • 另一方面,当数据量很大很大很大时,单表数据量可能会受到限制,需要分表,多个表之间的ID自增策略受限

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUDx8NLF-1688908161475)(【Redis】秒杀业务设计与分析/image-20230708120919560.png)]

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 根据KeyPrefix生成Id,key为 "icr:" + keyPrefix + ":" + date,每天一个Key,方便统计订单量
     * @param keyPrefix
     * @return
     */
    public long nextId(String keyPrefix) {
        
        // 1、生成时间戳
        LocalDateTime now = LocalDateTime.now();
        // 转换成当前的秒数
        long second = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = second - BEGIN_TIMESTAMP;

        // 2、构造存入的key,并增加count值
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3、拼接并返回
        return timeStamp << COUNT_BITS | count;
    }
}

测试:

	// 建一个线程池
    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {
        // 如果没有CountDownLatch ,
        // 由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,
        // 我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch
        CountDownLatch latch = new CountDownLatch(300);

        // 所以使用await可以让main线程阻塞,什么时候main线程不再阻塞呢?
        // 当CountDownLatch内部维护的变量变为0时,就不再阻塞,直接放行,
        // 调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,
        // 当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞
        
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };

        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }
  • Runnable接口是一个函数式接口,即只有一个方法。可以通过Lambda函数指定其run方法对应的代码。

2 秒杀下单

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eyYhbfF6-1688908161476)(【Redis】秒杀业务设计与分析/image-20230708224926473.png)]

  • Q1:是否在抢购时间内
  • Q2:库存是否充足
  • Q3:多个用户并发访问同一张优惠券,需要加锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiXbwuRI-1688908161476)(【Redis】秒杀业务设计与分析/image-20230708232049291.png)]

乐观锁

更新数据时去判断有没有其他线程对数据进行了修改。

版本号法:设置一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1,意味着操作过程中没有人对他进行过修改,则进行操作成功。

CAS法(compare and set):直接使用Stock进行判断,检查修改时库存是否大于0。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mjCxJfYb-1688908161477)(【Redis】秒杀业务设计与分析/image-20230708232543952.png)]

	@Override
    @Transactional(rollbackFor = Exception.class)
    public Result seckillVoucher(Long voucherId) {
        // 1、查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        LocalDateTime now = LocalDateTime.now();
        LocalDateTime begin = seckillVoucher.getBeginTime();
        LocalDateTime end = seckillVoucher.getEndTime();

        if (now.isBefore(begin)) {
            return Result.fail("秒杀尚未开始!");
        }

        if (now.isAfter(end)) {
            return Result.fail("秒杀已经结束!");
        }

        int stock = seckillVoucher.getStock();
        if (stock <= 0) {
            return Result.fail("库存不足");
        }

        // 2、购买优惠券,加入Order表,Stock更新
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0).update();

        if (!success) {
            return Result.fail("库存不足");
        }

        VoucherOrder voucherOrder = new VoucherOrder();
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);
    }

一人一单:悲观锁

  • 当同一个用户同时向数据库发送多条相同请求时,由于多个请求查找到数据库的结果相同,多个请求均有可能满足条件进行购买,从而产生错误。
  • 需要对同一个user的购买操作加锁。

将购买逻辑(是否购买过,更新stock,加入order表)封装为一个事务,必须把查询订单信息放在这个函数里,而不是外面。如果先在外面判断是否购买过优惠券,再放入该函数,相当于没有加上锁:

	@Override
    @Transactional(rollbackFor = Exception.class)
    public Result createVoucherOrder(Long voucherId) {
        // 2、查询订单信息:
        Long userId = UserHolder.getUser().getId();
        long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0) {
            return Result.fail("您已购买过这张优惠券,不能重复购买");
        }

        // 3、购买优惠券,Stock更新,加入Order表
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0).update();

        if (!success) {
            return Result.fail("库存不足");
        }

        VoucherOrder voucherOrder = new VoucherOrder();
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);
    }

2、如果直接在上述方法上加锁,锁的粒度太粗,不同的用户进入该方法时也会被锁住。因此在调用上述方法时,对userId.toString.intern()加锁,保证相同的userId从常量池中拿到的数据为同一个对象。同时,为了使事务注解生效,需要调用代理对象 AopContext.currentProxy()而不是该对象本身的方法。

	@Override
    public Result seckillVoucher(Long voucherId) {
        // 1、查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);

        LocalDateTime now = LocalDateTime.now();
        LocalDateTime begin = seckillVoucher.getBeginTime();
        LocalDateTime end = seckillVoucher.getEndTime();

        if (now.isBefore(begin)) {
            return Result.fail("秒杀尚未开始!");
        }

        if (now.isAfter(end)) {
            return Result.fail("秒杀已经结束!");
        }

        int stock = seckillVoucher.getStock();
        if (stock <= 0) {
            return Result.fail("库存不足");
        }

        // 2、查询订单信息:
        Long userId = UserHolder.getUser().getId();
        
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

    }

由于默认不可获得代理对象,需要在启动类上加入注释:

@EnableAspectJAutoProxy(exposeProxy = true)

并加入maven依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

3 分布式锁

满足分布式系统或集群模式下多进程可见并且互斥的锁。

使用MySQL比较少,Redis和Zookeeper比较常见。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Hhc74ZV-1688908161477)(【Redis】秒杀业务设计与分析/image-20230709142035406.png)]

3.1 Redis实现分布式锁

  • 获取锁:

    • 互斥:确保只能有一个线程获取锁

    • 非阻塞:尝试一次,成功返回true,失败返回false

  • 释放锁:

    • 手动释放:只能释放属于该线程的锁
    • 超时释放:获取锁时添加一个超时时间

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y30rWwUQ-1688908161477)(【Redis】秒杀业务设计与分析/image-20230709145851592.png)]


@Component
public class SimpleRedisLock {
    /**
     * 标识这个锁
     */
    private System name;
    private static final String KEY_PREFIX = "lock:";
    /**
     * 由于不同的JVM可能有相同的线程号,所以需要ID_PREFIX来表示属于哪个服务,拼接threadId来唯一标识线程
     */
    private static final String ID_PREFIX = UUID.randomUUID() + "-";

    @Resource
    StringRedisTemplate stringRedisTemplate;

    public boolean tryLock(long timeoutSec) {
        // 设置锁的值为获得当前锁的线程
        long threadId = Thread.currentThread().threadId();
        // 尝试获得锁,设置锁的过期时间以防止死锁
        return Boolean.TRUE.equals(
                stringRedisTemplate.opsForValue()
                        .setIfAbsent(KEY_PREFIX + name, ID_PREFIX + threadId, 
                                     timeoutSec, TimeUnit.SECONDS)
        );
    }

    public void unlock() {
        // 先判断当前线程有没有资格删掉这个锁,即redis中存储的线程id和当前线程id是否一致
        String threadId = ID_PREFIX + Thread.currentThread().threadId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 如果这个锁确实是当前服务器上 & 当前线程的锁
        if (threadId.equals(id)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

}
        Long userId = UserHolder.getUser().getId();

        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);

        // 获取锁失败,已有线程进入该段逻辑
        if (!lock.tryLock(1200)) {
            return Result.fail("请勿重复下单");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
        finally {
            lock.unlock();
        }

启动两个SpringBoot服务模拟分布式进行测试:

E:\leetcode\project_pre\Dianping\Front\conf\nginx.conf

			# proxy_pass http://127.0.0.1:8081;
            proxy_pass http://backend;
        }
    }

    upstream backend {
        server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
        server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
    }  

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAFwYfyy-1688908161477)(【Redis】秒杀业务设计与分析/image-20230709154854091.png)]

  • 可以使用LUA脚本进一步保证拿锁/还锁的原子性

3.2 Redisson实现分布式锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-El9KrSFN-1688908161478)(【Redis】秒杀业务设计与分析/image-20230709163829087.png)]

3.2.1 maven依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

3.2.2 使用示例

 @Test
    void testRedisson() throws Exception{
        // 创建锁对象
        RLock lock = redissonClient.getLock("anyLock");
        //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        //判断获取锁成功
        if(isLock){
            try{
                System.out.println("执行业务");
            }finally{
                //释放锁
                lock.unlock();
            }
        }
    }

ServiceImpl

		RLock lock = redissonClient.getLock("order:" + userId);

        // 获取锁失败,已有线程进入该段逻辑
        if (!lock.tryLock()) {
            return Result.fail("请勿重复下单");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
        finally {
            lock.unlock();
        }

3.3 Redisson可重入锁原理

逻辑如下右图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ffxDW1t-1688908161478)(【Redis】秒杀业务设计与分析/image-20230709175343635.png)]

method1调用method2,一个线程连续两次获取锁:重入。

在Lock锁中借助底层的一个voaltile的state变量来记录重入的状态。

  • 比如当前没有人持有这把锁,那么state=0
  • 假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,
  • 释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有

采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有。

KEYS[1]: 锁名称

ARGV[1]: 锁失效时间

ARGV[2] id + ":" + threadId,锁的小key

			 如果当前这把锁不存在,向redis中写一个hash数据并设置expire
			  "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
			  如果这个锁已经存在,通过大key + 小key判断当前这把锁是否是属于自己的,如果是自己的,+1,重置锁时间
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);"

3.4 Redisson锁重试与WatchDog机制

waitTime,leaseTime

boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);

第一个参数为重试等待时间,加入该参数以后,成为一个可重试的锁。

第二个参数为持有锁时间,默认为30s。

img

3.5 主从一致性问题

为了提高Redis的可用性,我们会搭建集群或者主从。

以主从为例:我们执行写命令,写在主机上, 主机会将数据同步给从机。但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就丢失了

为了解决这个问题,Redisson提出来了MutiLock锁,每个节点的地位都是一样的, 加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功。

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

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

相关文章

【Linux指令集】---cp指令(超详细)

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【Linux专栏】&#x1f388; 本专栏旨在分享学习Linux的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 演示环境&#xff1…

cmake include命令

目录 cmake include命令 CMAKE_CURRENT_LIST_DIR: CMAKE_CURRENT_LIST_FILE: CMAKE_CUEERNT_LIST_LINE: demo top CMakeLists.txt test.cmake subDir下的 CMakeLists.txt 结果: cmake include命令 include()命令也是将一个新的cmake内容引入当前cmake内容中. 格式: …

JVM理论(一)基础概念

JVM概述 JVM就是二进制字节码的运行环境,负责装载字节码到其内存,解释/编译为对应平台上的机器指令执行,每条java指令在java虚拟机规范中都有详细定义,包括如何取、处理操作数等;JVM特点如下 一次编译,到处运行&#xff08;各CPU的架构不同的情况下JVM为了实现跨平台,字节码指…

删除链表的倒数第 N 个结点——力扣19

题目描述 法一&#xff09;计算链表长度 class Solution { public:int getLength(ListNode* head){int len0;while(head){len;head head->next;}return len;}ListNode* removeNthFromEnd(ListNode* head, int n) {int len getLength(head);ListNode* dummy new ListNode …

汽车架构解析:python解析Autosar架构的ARXML

文章目录 前言一、Container-I-PDU概念引入二、以文本形式读取ARXML文件三、解析Frame的基本参数四、解析Frame中的PDU五、解析PDU中的Signals六、解析Signal中的初始值和长度七、解析Signal中的起始位置八、解析Signal中的枚举值或公式九、解析ARXML总结 前言 Autosar架构下a…

什么是人工智能?

人工智能基本定义&#xff1a; 人工智能&#xff08;Artificial Intelligence&#xff09;&#xff0c;英文缩写为AI。 它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。人工智能是新一轮科技革命和产业变革的重要驱动力量。 人工…

美团面试官:可重复读隔离级别实现原理是什么?(一文搞懂MVCC机制)

本文首发于公众号【看点代码再上班】&#xff0c;欢迎围观&#xff0c;第一时间获取最新文章。 原文&#xff1a;美团面试官&#xff1a;可重复读隔离级别实现原理是什么&#xff1f;&#xff08;一文搞懂MVCC机制&#xff09; “全文共计4270字&#xff0c;预计阅读时间6分钟 …

MATLAB | 爱心图例与精致半透明圆角图例

本文中全部示意图均为本人自制&#xff0c;传播时请注明出处。 写了点小成品函数&#xff0c;比如如何绘制饼图时把图例上的图标变成心形&#xff1a; 比如如何自制半透明圆角图例&#xff1a; 依旧先讲原理再给出这俩代码哈&#xff1a; 1 原理讲解-图形对象 封闭四边形对…

3.7.使用cuda核函数加速warpaffine

目录 前言1. warpAffine2. warpAffine案例2.1 导言2.2 main函数2.3 warpaffine_to_center_align函数2.4 warp_affine_bilinear函数2.5 warp_affine_bilinear_kernel核函数2.6 AffineMatrix结构体 3. 补充知识总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0…

【滨小之旅搜索】八皇后

题目传送门 [USACO1.5] 八皇后 Checker Challenge 题目描述 一个如下的 6 6 6 \times 6 66 的跳棋棋盘&#xff0c;有六个棋子被放置在棋盘上&#xff0c;使得每行、每列有且只有一个&#xff0c;每条对角线&#xff08;包括两条主对角线的所有平行线&#xff09;上至多有…

js中判断一个对象是否存在

一、Boolean()方法 用Boolean()方法可以将Js中的任意数据类型转为布尔值&#xff1a; 二、用于判断xx是否存在 js一般会自动执行Boolean()方法&#xff0c;我们可以借此判断某个对象在js当前的执行环境中知否存在。如&#xff1a; var x 1; // x的数据类型为数值 if (x) { …

Microsoft 宣布今年底关闭开源软件托管平台 CodePlex

Microsoft 宣布&#xff0c;将关闭开源软件托管平台 CodePlex。Microsoft 2006 年推出这项服务&#xff0c;并决定在今年 12 月 15 日将其关闭。 Microsoft 公司副总裁 Brian Harry 在网上博客中写道&#xff0c;人们将可以下载他们的数据档案&#xff0c;Microsoft 正与面向开…

Vue3 动态路由、动态组件使用示例

前期回顾 Vue3 TS Vite —— 大屏可视化 项目实战_vue3可视化大屏_彩色之外的博客-CSDN博客大屏可视化项目实战_vue3可视化大屏https://blog.csdn.net/m0_57904695/article/details/131014666?spm1001.2014.3001.5501 目录 &#x1f44d; 动态组件 &#x1f440; 动态路由…

链接做网络互动酷投票平台网络投票

关于微信投票&#xff0c;我们现在用的最多的就是小程序投票&#xff0c;今天的网络投票&#xff0c;在这里会教大家如何用“活动星投票”小程序来进行投票。 我们现在要以“读好书助成长”为主题进行一次投票活动&#xff0c;我们可以在在微信小程序搜索&#xff0c;“活动星投…

3.9.错误处理的理解以及错误的传播特性

目录 前言1. thrust2. error总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习精简 CUDA 教程-错误处理的理解以及错误的传播…

机械臂与RealSense相机手眼标定

环境&#xff1a; 本文主要使用kinova mico机械臂 RealSense D435i深度相机进行了eye to hand的手眼标定。 系统环境&#xff1a;Ubuntu18.04&#xff0c;ROS Melodic 硬件&#xff1a;Kinova mico&#xff0c;RealSense D435i 特别注意&#xff1a;经测试&#xff0c;本方法…

大人,时代变了!做测试也要懂Python开发!(文末送书五本)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

一道有趣的C语言指针笔试题

一道有趣的指针笔试题 注&#xff1a;博主认为&#xff0c;如果想要做对并理解这一题的有关知识点&#xff0c;读者有必要对指针有一个较为全面且深刻的认识&#xff0c;如果小伙伴们对指针还不是太熟悉&#xff0c;建议先看看一篇带你玩转C语言指针&#xff1a;从入门到精通 …

OpenCV 入门教程:自适应阈值处理

OpenCV 入门教程&#xff1a;自适应阈值处理 导语一、自适应阈值处理二、示例应用2.1 图像二值化2.2 图像去噪 总结 导语 自适应阈值处理是图像处理中常用的技术之一&#xff0c;它能够根据图像的局部特征自动调整阈值&#xff0c;从而提高图像的处理效果。在 OpenCV 中&#…

手把手教你云相册项目简易开发 day1 Kafka+IDEA+Springboot+Redis+MySQL+libvips 简单运行和使用

项目的创建 项目采用的是微服务的架构。先创建一个父项目cloud-photo&#xff0c;然后再在module下创建api、image、users的子项目 相关配置&#xff1a; application.yml。此处如果没有redis的话可以先注释掉&#xff0c;因为后面启动需要mysql连接成功和redis服务启动 spr…