Redis 优化秒杀(异步秒杀)

news2025/1/11 0:36:18

目录

为什么需要异步秒杀

异步优化的核心逻辑是什么?

阻塞队列的特点是什么?

  Lua脚本在这里的作用是什么?

异步调用创建订单的具体逻辑是什么?

为什么要用代理对象proxy调用createVoucherOrder方法?

对于代码的详细解释:

SECKILL_ORDER_EXECUTOR 是什么?

@PostConstruct 是什么?

VoucherOrderHandler 是什么?

VoucherOrderHandler 调用的handleVoucherOrder:

数据库操作的注意点有哪些?

seckillVoucher 方法: 

单线程线程池、阻塞队列、seckillVoucher 和 VoucherOrderHandler 的协作过程总结

方法调用流程总览

方法逻辑一览表

完整代码 


在秒杀场景中,我们可以将库存存入 Redis,并通过 Lua 脚本来判断用户是否有秒杀资格,同时实现一人一单的限制。由于 Redis 的单线程特性和 Lua 脚本的原子性保障,能够避免多个线程交叉执行 Redis 命令导致的并发问题。同时,使用阻塞队列将订单请求进行缓冲,当线程尝试从队列中获取订单时,如果队列为空,线程会被阻塞,直到有新订单加入队列,线程才会被唤醒并处理订单,从而实现高效的生产者-消费者模型。

为什么需要异步秒杀

1. 防止数据库压力过载

  • 异步秒杀通过将订单请求写入阻塞队列,削峰填谷,避免将瞬时高并发请求直接传递到数据库。
  • 消费者线程从队列中按顺序取出订单进行处理,减少数据库同时处理的请求量。

2. 提升系统响应速度

  • 秒杀请求在异步架构中:
    1. 同步部分:快速返回秒杀结果(例如秒杀资格校验)。
    2. 异步部分:订单的具体处理(如扣减库存、保存订单)放到后台处理。
  • 这种分离让用户能快速得到响应,而系统后台有更多时间处理复杂的订单逻辑。

异步优化的核心逻辑是什么?

问:为什么需要异步优化秒杀订单? 答:在高并发场景中,秒杀会同时产生大量订单请求。如果直接将请求交给数据库处理,容易导致数据库压力过大,从而系统崩溃。异步优化通过使用阻塞队列将订单请求排队,避免直接对数据库产生瞬时高负载。

问:如何实现异步处理? 答:将订单信息保存到阻塞队列中,使用单线程(线程池中的线程)从队列中按顺序取出订单进行处理。这样可以削峰填谷,减轻数据库压力。


阻塞队列的特点是什么?

问:阻塞队列的作用是什么? 答:阻塞队列是线程安全的队列,支持生产者-消费者模型在代码中,生产者是seckillVoucher方法,它将订单信息加入阻塞队列;消费者是VoucherOrderHandler线程,它从队列中取出订单进行处理。

问:为什么使用阻塞队列? 答:阻塞队列的特点是,如果队列为空,消费者线程会阻塞等待;如果队列满了,生产者线程会阻塞等待。这样可以很好地协调生产者和消费者的速度,避免资源浪费或超负荷。


  Lua脚本在这里的作用是什么?

问:为什么使用Lua脚本操作Redis? 答:Lua脚本在Redis中是原子执行的。使用Lua脚本可以保证秒杀资格验证和库存扣减的原子性,避免并发问题。

问:Lua脚本验证了什么? 答:

  1. 用户是否重复下单(通过Redis中存储的用户信息判断)。
  2. 秒杀库存是否充足(通过Redis中存储的库存数量判断)。
-- 参数
-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]

-- 数据key
local stockKey = 'seckill:stock:'.. voucherId
local orderKey = 'seckill:order:'.. voucherId

-- 检查库存是否足够
if (tonumber(redis.call('get', stockKey)) <= 0) then
    return 1 -- 库存不足
end

-- 检查用户是否重复下单
if (redis.call('sismember', orderKey, userId) == 1) then
    return 2 -- 重复下单
end

-- 减少库存并记录订单
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
return 0

将秒杀券的库存以String形式存入Redis

 @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
        //  保存秒杀 库存 存入Redis当中
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(),voucher.getStock().toString());
    }

异步调用创建订单的具体逻辑是什么?

@Override
public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();

    // 1. 校验秒杀资格
    Long res = stringRedisTemplate.execute(
        SECKILL_SCRIPT,
        Collections.emptyList(),
        voucherId.toString(),
        userId.toString()
    );

    if (res != 0) {
        // 秒杀资格校验失败
        return Result.fail(res == 1 ? "库存不足" : "重复下单");
    }

    // 2. 生成订单信息
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderID = redisIdWorker.nextId("order");
    voucherOrder.setId(orderID);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);

    // 3. 将订单信息放入阻塞队列
    orderTasks.add(voucherOrder);

    // 获取代理对象
    proxy = (IVoucherOrderService) AopContext.currentProxy();

    return Result.ok(orderID);
}

问:seckillVoucher方法中发生了什么? 答:这是异步调用的入口逻辑,分为以下几个步骤:

  1. 验证秒杀资格
    • 使用Lua脚本操作Redis,确保原子性。
    • 判断用户是否重复下单,或者库存是否不足。
    • 如果秒杀资格验证失败,则直接返回错误信息。
  2. 生成订单信息
    • 使用RedisIdWorker生成订单ID。
    • 将订单信息(用户ID、代金券ID等)封装成VoucherOrder对象。
  3. 将订单信息保存到阻塞队列
    • 调用orderTasks.add(voucherOrder)将订单加入阻塞队列中。
  4. 返回订单ID
    • 在返回给用户订单ID时,并没有真正完成订单,而是进入队列等待处理。

为什么要用代理对象proxy调用createVoucherOrder方法?

问:为什么不直接调用createVoucherOrder

答:因为 createVoucherOrder 方法是事务方法,需要通过代理对象调用才能生效。

  1. Spring 的事务机制基于 AOP(面向切面编程)实现

    • Spring 使用代理对象(动态代理或 CGLIB 代理)来拦截对事务方法的调用,并在方法执行前后添加事务管理逻辑(如开启事务、提交事务或回滚事务)。
    • 如果直接调用类内部的事务方法,调用不会经过代理对象,而是直接执行原始方法,Spring 的事务管理器无法介入,导致事务逻辑失效。
  2. 内部调用的问题

    • 在类的内部直接调用另一个事务方法时,调用不会经过代理对象,而是通过 this 调用,因此事务拦截器不会生效,事务注解(@Transactional)失效。

问:代理对象是如何获取的?

  • 将代理对象声明为一个成员变量,通过 AopContext.currentProxy() 获取当前类的代理对象。
  • 原因AopContext.currentProxy() 返回的是 Spring AOP 生成的当前类的代理对象,它能够拦截方法调用,从而触发事务管理逻辑。
  • 在异步线程中直接调用当前类的方法时,事务不会生效,因为直接调用是通过 this 引用,而不是代理对象调用。通过成员变量保存的代理对象,即使在异步线程中调用方法,也可以确保事务逻辑有效。
  • 最终,通过代理对象调用 createVoucherOrder 方法,可以正常触发 Spring 的事务管理器,确保事务功能生效。


对于代码的详细解释:

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

private final BlockingQueue<VoucherOrder> orderTasks = new LinkedBlockingQueue<>();
@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
   
private class VoucherOrderHandler implements Runnable {
     @Override
       public void run() {
          while (true) {
              try {
                  // 获取队列当中的订单
                  VoucherOrder voucherOrder = orderTasks.take();
                  handleVoucherOrder(voucherOrder);
              } catch (Exception e) {
                  log.error("Error processing order", e);
              }
          }
      }
  }

SECKILL_ORDER_EXECUTOR 是什么?

SECKILL_ORDER_EXECUTOR 是一个 单线程线程池,用来处理秒杀订单的异步任务。

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

单线程线程池的特点是:线程池中始终只有一个线程,任务会按顺序执行,适合需要顺序处理的场景。

它的主要作用是 管理和调度线程的生命周期。具体来说:

启动和管理消费者线程

  • VoucherOrderHandler 需要一个线程不断运行,用来从阻塞队列中取订单并处理。
  • 线程池 SECKILL_ORDER_EXECUTOR 的作用是启动这个线程,并保证这个线程的生命周期由线程池管理。

线程复用

  • 如果你手动创建线程(new Thread()),可能会导致系统频繁创建和销毁线程,浪费系统资源。
  • 使用线程池可以复用线程,减少线程的创建和销毁开销,提高性能。

稳定性

  • 如果 VoucherOrderHandler 线程在执行中意外退出(例如抛出未捕获异常),线程池会自动接管并重新启动线程,保证任务不会中断。

在这里,SECKILL_ORDER_EXECUTOR 通过单线程的方式从阻塞队列中取出订单,按顺序处理,确保秒杀订单的处理逻辑是线程安全的。


@PostConstruct 是什么?

@PostConstruct 是 Java 的一个注解,作用是在 Spring 容器将 Bean 初始化完成后,立即执行标注的方法。换句话说,当 Spring 加载并创建了 VoucherOrderServiceImpl 实例后,会自动调用 init() 方法。

这是一个生命周期回调方法,常用于初始化逻辑,比如启动线程、加载配置等。


 init() 方法的作用是什么?

  • 这个方法的主要作用是 启动一个专用线程(由单线程线程池管理),用于从阻塞队列中取出订单并进行异步处理。
  • 通过 SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());,将 VoucherOrderHandler 提交到线程池中,线程池会启动一个线程,持续运行 VoucherOrderHandler 中的逻辑。

VoucherOrderHandler 是什么?

VoucherOrderHandler 是一个内部类,它实现了 Runnable 接口,代表一个任务。

  • 任务的核心逻辑是:从阻塞队列中取出订单并处理
  • 它的 run() 方法包含一个 while(true) 循环,这样线程会一直运行,不断从队列中取出订单(通过 orderTasks.take()),直到程序终止。

VoucherOrderHandler 调用的handleVoucherOrder:

  • 防止同一用户多次下单(重复下单)。
  • 调用执行订单的具体业务逻辑的方法createVoucherOrder
    (如扣减库存、保存订单等)。
        @Transactional
        public void createVoucherOrder(VoucherOrder voucherOrder) {
            // 实现一人一单,我们需要先判断该用户是否已经抢过了
            // 根据优惠券id和用户id查询订单
            Long userId = UserHolder.getUser().getId();
            int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
            if (count > 0) {
               log.error("已经购买过,不可重复购买!");
            }
            // 扣减库存
            boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder).
    //                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库
            gt("stock", 0).update();
            if (!success) {
                log.error("库存不足!");
            }
            // 写入数据库 不需要再返回orderId了,因为之前在seckillVoucher已经返回了
            save(voucherOrder);
        }

数据库操作的注意点有哪些?

问:如何实现一人一单的限制? 答:在createVoucherOrder方法中,通过查询数据库判断用户是否已经购买过对应的代金券。

问:如何扣减库存? 答:使用seckillVoucherService执行SQL语句更新库存,并通过gt(\"stock\", 0)确保库存大于0。


整体逻辑总结

  • 触发时机: 当 VoucherOrderServiceImpl 被 Spring 加载并实例化后,@PostConstruct 注解标注的 init() 方法会被调用。

  • 作用init() 方法向线程池提交了一个 VoucherOrderHandler 任务,这个任务会启动一个线程,不断从阻塞队列中取出订单并调用相关处理逻辑(handleVoucherOrder)。


seckillVoucher 方法: 

单线程线程池、阻塞队列、seckillVoucher VoucherOrderHandler 的协作过程总结

seckillVoucher 是厨师

  • 它负责接收顾客的订单请求(秒杀请求),检查是否符合要求(库存是否足够、是否重复下单),然后生成订单(菜品)并放在桌子上(阻塞队列)。
  • 核心职责:生产订单,确保每个订单合法并生成完整订单信息。

BlockingQueue 是桌子

  • 它负责临时存放厨师制作好的订单(菜品),保证每个订单都按顺序排列。
  • 如果桌子空了,顾客(消费者线程)只能等;如果桌子满了,厨师(生产者线程)也需要暂停制作。
  • 核心职责:缓冲区,用于在生产和消费之间解耦。

VoucherOrderHandler 是顾客

  • 它负责从桌子上取菜(从队列中取订单),并最终消费(处理订单,包括扣减库存、写入数据库等)。
  • 如果桌子没有菜了,它会耐心等待;一旦有菜,它会立刻取走并处理。
  • 核心职责:消费订单,执行订单处理逻辑。

SECKILL_ORDER_EXECUTOR 是服务员

  • 它负责启动和管理顾客(消费者线程),确保顾客始终在桌子旁边等待取菜。
  • 如果顾客突然有事不要菜品了(比如异常退出),服务员会招待一个新的顾客来接替。
  • 核心职责:管理消费者线程的生命周期,确保订单处理不断运行。


方法调用流程总览

  1. 用户发起秒杀请求,触发 seckillVoucher 方法。
  2. seckillVoucher 验证秒杀资格并将订单放入阻塞队列。
  3. VoucherOrderHandler(由线程池管理的消费者线程)从队列中取出订单,调用 handleVoucherOrder 进行处理。
  4. handleVoucherOrder 利用分布式锁防止重复下单,并调用 createVoucherOrder 完成订单的核心逻辑。
  5. createVoucherOrder 执行订单的最终处理,包括扣减库存、写入数据库等。

方法逻辑一览表

方法作用关键逻辑
seckillVoucher秒杀请求入口,生成订单并加入阻塞队列验证秒杀资格,生成订单信息,加入阻塞队列。
阻塞队列 (BlockingQueue)存储订单信息,实现生产者与消费者的解耦线程安全存储,缓冲生产者和消费者速度差异。
VoucherOrderHandler消费者线程,从队列中取订单并调用处理方法从队列取订单,调用 handleVoucherOrder
handleVoucherOrder防止重复下单,调用核心业务逻辑创建分布式锁,防止重复下单,调用 createVoucherOrder
createVoucherOrder执行订单的核心逻辑校验订单、扣减库存、保存订单到数据库。

完整代码 

@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    final RedisIdWorker redisIdWorker;
    final StringRedisTemplate stringRedisTemplate;
    final RedissonClient redissonClient;

    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
    private IVoucherOrderService proxy;
    private final BlockingQueue<VoucherOrder> orderTasks = new LinkedBlockingQueue<>();

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    // 获取队列当中的订单
                    VoucherOrder voucherOrder = orderTasks.take();
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("Error processing order", e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();

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

        // 2. Try to acquire lock
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("Duplicate order not allowed");
            return;
        }

        try {
            // 3. Create order via proxy
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 4. Release lock
            lock.unlock();
        }
    }

    /**
     * 基于异步Lua脚本保证原子性
     *
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        // 执行Lua脚本
        Long res = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString()
        );
        // 判断返回值是否为0
        if (res != 0) {
            // 非0 则没有秒杀资格
            return Result.fail(res == 1 ? "库存不足" : "重复下单");
        }
        // 从Redis当中获取下单信息
        long orderId = redisIdWorker.nextId("order");
        // TODO 为0 表示有秒杀资格 需要将下单信息保存在阻塞队列当中
        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id
        long orderID = redisIdWorker.nextId("order");
        voucherOrder.setId(orderID);
        // 用户id
        voucherOrder.setUserId(UserHolder.getUser().getId());
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        // 保存到阻塞队列当中
        orderTasks.add(voucherOrder);

        // 获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }
    
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 实现一人一单,我们需要先判断该用户是否已经抢过了
        // 根据优惠券id和用户id查询订单
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
        if (count > 0) {
           log.error("已经购买过,不可重复购买!");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder).
//                eq("stock",voucher.getStock()). // 加个乐观锁,如果现在的库存和我之前查询的库存不相同,说明在我之前就有线程修改了数据库
        gt("stock", 0).update();
        if (!success) {
            log.error("库存不足!");
        }
        // 写入数据库
        save(voucherOrder);
    }
}

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

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

相关文章

Python 中的错误处理与调试技巧

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

关于腾讯4K算法搭建使用

准备国内服务器一台&#xff0c;轻量服务器请尽量开全端口安装linux,centos7.6-7.9系统&#xff0c;记住纯净系统&#xff0c;然后安装宝塔宝塔安装环境为nginx1.24,7.2(PHP版本没有要求)&#xff0c;Mysql5.7(没有要求) 准备活动完毕&#xff01;&#xff01;&#xff01; 上传…

工艺参数优化、工程设计优化!GRNN神经网络+NSGAII多目标优化算法(Matlab)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.GRNN神经网络NSGAII多目标优化算法&#xff0c;工艺参数优化、工程设计优化&#xff08;Matlab完整源码和数据&#xff09; 多目标优化是指在优化问题中同时考虑多个目标的优化过程。在多目标优化中&#xff0c;通…

【Rust自学】11.6. 控制测试运行:并行和串行(连续执行)测试

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 11.6.1. 控制测试的运行方式 cargo test和cargo run一样&#xff0c;cargo test也会编译代…

nginx负载均衡-基于端口的负载均衡(一)

注意&#xff1a; (1) 做负载均衡技术至少需要三台服务器&#xff1a;一台独立的负载均衡器&#xff0c;两台web服务器做集群 一、nginx分别代理后端web1 和 web2的三台虚拟主机 1、web1&#xff08;nginx-10.0.0.7&#xff09;配置基于端口的虚拟主机 [rootOldboy extra]# …

DDcGAN_多分辨率图像融合的双鉴别条件生成对抗网络_y译文马佳义

摘要&#xff1a; 在本文中&#xff0c;我们提出了一种新的端到端模型&#xff0c;称为双鉴别条件生成对抗网络&#xff08;DDcGAN&#xff09;&#xff0c;用于融合不同分辨率的红外和可见光图像。我们的方法建立了一个生成器和两个鉴别器之间的对抗博弈。生成器的目的是基于特…

【C++/控制台】2048小游戏

源代码&#xff1a; #include <iostream> #include <windows.h> #include <stdio.h> #include <math.h> #include <stdlib.h> #include <conio.h> #include <time.h>// #define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME)…

【Rust自学】11.5. 在测试中使用Result<T, E>

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 11.5.1. 测试函数返回值为Result枚举 到目前为止&#xff0c;测试运行失败的原因都是因为触…

最新版IDEA新建web项目--小白也能看懂

引言&#xff1a; 此方法适用于 IntelliJ IDEA 2024.1.4 最新版本。 我最初使用的是 Tomcat 8.0.23 版本&#xff0c;搭配 JDK 17。由于 Tomcat 8.0.23 使用了已经被弃用的 JVM 参数&#xff0c;故将 Tomcat 版本更换为 10.1.1。 如果你使用 JDK 17&#xff0c;建议使用 Tom…

ue5玩家角色添加武器。切换武器位置,手上武器放到背上。演示一下人体插槽和武器的连接。仅仅演示,实际项目不是这么用的

把第一人称资源包导进来 这就是我们枪的骨骼网格体 我们找到这个骨骼 右手添加插槽 取个名字 因为武器上也有动画&#xff0c;所有武器单独写个蓝图类 新建一个蓝图类 BP_Weapon 把枪的蓝图拖到人的静态网格体下&#xff0c;成为一个部分 选中BP_Weapon的父类套接字…

微信小程序防止重复点击事件

直接写在app.wpy里面&#xff0c;全局可以调用 // 防止重复点击事件preventActive(fn) {const self this;if (this.globalData.PageActive) {this.globalData.PageActive false;if (fn) fn();setTimeout(() > {self.globalData.PageActive true;}, 3000); //设置该时间内…

Docker入门之docker基本命令

Docker入门之docker基本命令 官方网站&#xff1a;https://www.docker.com/ 1. 拉取官方镜像并创建容器&#xff08;以redis为例&#xff09; 拉取官方镜像 docker pull redis# 如果不需要添加到自定义网络使用这个命令&#xff0c;如需要&#xff0c;直接看第二步 docker r…

SQL Server中可以通过扩展事件来自动抓取阻塞

在SQL Server中可以通过扩展事件来自动抓取阻塞&#xff0c;以下是详细流程&#xff1a; 开启阻塞跟踪配置&#xff1a; • 执行以下SQL语句来启用相关配置&#xff1a; EXEC sp_configureshow advanced options, 1; RECONFIGURE; EXEC sp_configure blocked process thresh…

【VBA】【EXCEL】将某列内容横向粘贴到指定行

Sub CopyRowToColumn()On Error GoTo ErrorHandler 添加错误处理Application.ScreenUpdating FalseApplication.Calculation xlCalculationManualApplication.EnableEvents False 禁用事件处理Dim lastCol As LongDim lastRow As LongDim i As Long, colCount As LongDim …

基于机器学习的故障诊断(入门向)

一、原始信号的特征提取 1.EMD经验模态分解的作用 信号分析&#xff1a;EMD可以将信号分解为多个IMFs&#xff0c;每个IMF代表信号中的一个特定频率和幅度调制的成分。这使得EMD能够提供对信号的时频特征进行分析的能力&#xff08;特征提取用到的&#xff09;。信号去噪&…

多台PC共用同一套鼠标键盘

当环境中有多个桌面 pc 需要操作的时候&#xff0c;在 多台 pc 之间切换会造成很多的不方便 可以通过远程进行连接&#xff0c;但是有一个更好的方案是让多台机器之间共用同一套键盘鼠标 常用的解决方案 synergy 和 sharemouse&#xff0c;通过移动光标在不同的 pc 间切换 s…

[免费]微信小程序(高校就业)招聘系统(Springboot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序(高校就业)招聘系统(Springboot后端Vue管理端)&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序(高校就业)招聘系统(Springboot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项目介绍…

Midjourney 应用:框架总结

Midjourney 应用&#xff1a;框架总结 官方的模板很简单&#xff0c;分成四个部分&#xff1a; 主体细节 & 背景风格、媒介、艺术家参数 我的总结 其实按照官方模板写&#xff0c;你已经能超过 90% 的初学者&#xff0c;但根据我的实验&#xff0c;我细化了他们的模板的…

【Maui】导航栏样式调整

前言 .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架&#xff0c;用于使用 C# 和 XAML 创建本机移动和桌面应用。 使用 .NET MAUI&#xff0c;可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。 .NET MAUI 是一款开放源代码应用&#xff0c;是 X…

uniapp 微信小程序内嵌h5实时通信

描述&#xff1a; 小程序webview内嵌的h5需要向小程序实时发送消息&#xff0c;有人说postMessage可以实现&#xff0c;所以试验一下&#xff0c;结果是实现不了实时&#xff0c;只能在特定时机后退、组件销毁、分享时小程序才能接收到信息&#xff08;小程序为了安全等考虑做了…