黑马点评秒杀优化

news2025/4/20 5:13:28

异步优化秒杀业务

        回顾之前的内容黑马点评 秒杀优惠券集群下一人一单超卖问题-CSDN博客,为了处理并发情况下的线程安全数据一致性的问题,我们已经完成了查询优惠券信息、判断秒杀是否开始和结束、检查库存、用户ID加锁、创建订单和扣减库存。

        尽管之前的代码已经解决了基础问题,但在高并发情景下仍有瓶颈,接下来就需要对性能稳定性做进一步的提升。所以接下来将使用异步优化秒杀业务。

为什么需要异步优化

1. 同步阻塞导致吞吐量低

  • 问题:用户请求需等待所有数据库操作完成(如订单写入、库存扣减),线程长时间阻塞,系统吞吐量受限。

  • 优化目标

    • 立即响应:用户请求快速返回,耗时操作异步执行。

    • 提升吞吐量:将数据库压力转移至后台,Tomcat 线程快速释放,可处理更多请求。

2. 数据库成为性能瓶颈

  • 问题:每次请求直接操作数据库,高并发时连接池耗尽、慢查询堆积。

  • 优化目标

    • 削峰填谷:通过消息队列缓存请求,平滑流量高峰。

    • 异步写库:批量处理订单,减少事务提交次数,降低数据库负载。

3. 用户体验差

  • 问题:用户需等待秒杀所有逻辑完成(通常200ms~1s),高并发时易超时或失败。

  • 优化目标

    • 快速反馈:用户点击后立即返回“请求已受理”,订单状态后续异步通知(如短信或站内信)。

补充:

同步:任务按顺序执行,必须等待前一个任务完成后,才能开始下一个任务。

  • 问题

    • 所有操作都在同一个线程中完成,用户必须等待所有步骤结束才能得到响应。

    • 高并发时,数据库压力大,响应时间变长,用户体验差。

异步:任务提交后立即返回,后续操作由其他线程或服务在后台处理。

  • 优势

    • 用户无需等待数据库操作完成,响应速度极快(毫秒级)。

    • 数据库压力被削峰填谷,避免高并发直接冲击。

优化任务如下:

阻塞队列Java BlockingQueue

1. 定义阻塞队列是一种线程安全的数据结构,能平衡生产者与消费者的处理速度,避免资源耗尽或数据丢失。它支持以下操作:

  • 入队(Put):队列满时,生产者线程会被阻塞,直到队列有空位。
  • 出队(Take):队列空时,消费者线程会被阻塞,直到队列有新数据。

2. 在秒杀业务中的使用:

  • 生产者:Tomcat 服务,快速校验后将订单任务写入队列。
  • 消费者:后台线程或服务,从队列中取出任务并处理(扣库存、写订单)。

seckill.lua:

-- 1.参数列表
-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]

-- 2.数据key
-- 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务逻辑
-- 判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 库存不足,返回1
    return 1
end
-- 判断用户是否已下单  SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 用户重复下单,返回2
    return 2
end

-- 扣减库存,并判断扣减是否成功 INCRBY stockKey -1
redis.call("incrby", stockKey, -1)
-- 下单,保存用户id到集合 SADD orderKey userId
redis.call("sadd", orderKey, userId)
return 0

改造后的VoucherServiceImpl.java:

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

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;


    /**
     * 加载Lua脚本:执行脚本保证原子性操作
     */
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

    //静态初始化块
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();   //创建实例
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));   //设置脚本文件位置
        SECKILL_SCRIPT.setResultType(Long.class);    //设置返回类型为Long
    }

    /**
     * 创建阻塞队列:异步下单,提高效率
     */
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

    /**
     * 创建单线程线程池
     */
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * 在VoucherServiceImpl类初始化时执行线程池
     */
    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    /**
     * 线程池异步执行优惠券订单任务
     */
    private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    //1.获取阻塞队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    //2.创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }


    /**
     * 创建订单
     *
     * @param voucherOrder
     */
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //1.获取用户id,这里是多线程不能在ThreadLocal中直接获得当前用户id
        Long userId = voucherOrder.getUserId();
        //2.创建锁对象(锁键设计:"lock:order:" + userId 以用户ID为粒度,不同用户请求可并行)
        RLock lock = redissonClient.getLock(LOCK_ORDER_KEY + userId);
        //3.获取锁(无参表示只尝试获取锁一次)
        boolean isLock = lock.tryLock();
        //4.判断获取锁是否成功
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单");
            return;
        }
        try {
            //获取锁成功,创建订单
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            //释放锁
            lock.unlock();
        }
    }


    private IVoucherOrderService proxy;

    /**
     * 抢购秒杀优惠券
     *
     * @param voucherId
     * @return
     */
    public Result seckillVoucher(Long voucherId) {
        //获取用户id
        Long userId = UserHolder.getUser().getId();
        //1.执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString());
        //2.判断结果是否为0
        //2.1不为0,代表没有抢购资格
        // result为1表示库存不足,result为2表示用户已下单
        int r = result.intValue();
        if (r != 0) {
            switch (r) {
                case 1:
                    return Result.fail("库存不足");
                case 2:
                    return Result.fail("不能重复下单");
                /*case 3:
                    return Result.fail("活动结束");*/
            }
        }
        //2.2 result为0,用户具有秒杀资格,将订单信息(订单id,优惠券id,用户id)保存到阻塞队列中,实现异步下单
        //3.创建订单(在订单表tb_voucher_order插入一条数据)
        VoucherOrder voucherOrder = new VoucherOrder();
        //3.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //3.2 用户id
        voucherOrder.setUserId(userId);
        //3.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        // 3.4放入阻塞队列
        orderTasks.add(voucherOrder);
        //4.获取代理对象,使用代理对象调用第三方事务方法,防止事务失效
        proxy = (IVoucherOrderService) AopContext.currentProxy();   //获取当前类的代理对象
        //5.返回订单id
        return Result.ok(orderId);
    }


    /**
     * 通过数据库查询确保“一人一单”
     *
     * @param voucherOrder
     */
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        //5.一人一单
        Long userId = voucherOrder.getUserId();
        //5.1查询数据库中是否已经存在该用户抢购该优惠券的订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        //5.2判断是否存在
        if (count > 0) {
            //用户已经购买过了,返回失败信息
            log.error("用户已购买!");
            return;
        }

        //6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")   //set stock = stock - 1
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0)   //where id = ? and stock > 0 数据库层面的乐观锁,避免超卖
                .update();

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

        //7.创建订单(在订单表tb_voucher_order插入一条数据)
        //插入到订单信息表
        save(voucherOrder);
    }
}

使用阻塞队列存在的问题

1. 内存限制与数据丢失风险
  • 内存溢出(OOM)
    阻塞队列存储在JVM内存中,当瞬时流量极大(如百万级请求),队列可能迅速占满内存,导致服务崩溃。

  • 数据丢失
    若服务宕机或重启,内存中的队列数据全部丢失,未处理的订单请求无法恢复。

2. 单机瓶颈
  • 无法水平扩展
    阻塞队列基于单机内存,无法跨多台服务器共享。当单机处理能力不足时,无法通过增加节点扩展吞吐量。

  • 消费者能力受限
    消费者线程池受限于单机CPU和内存资源,无法灵活应对流量高峰。

3. 可靠性不足
  • 无持久化机制
    消息仅存在于内存,缺乏磁盘持久化,无法应对服务崩溃或断电。

  • 缺乏重试与死信处理
    若消息处理失败(如数据库异常),无法自动重试或转移至死信队列,需手动实现补偿逻辑。

消息队列优化

        消息队列是一种在分布式系统中用于在不同组件或服务之间传递消息的中间件。其核心机制是生产者将消息发送到队列,消费者从队列中取出消息进行处理。这种模式实现了系统间的异步通信和解耦。

Redis消息队列

        Redis不仅是一个高性能的内存数据库,还提供了多种数据结构(如List、Pub/Sub、Stream)来实现轻量级的消息队列。

1. 基于List的简单队列
  • 核心命令:
    • LPUSH / RPUSH:生产者向列表左/右侧插入消息。
    • BRPOP / BLPOP:消费者阻塞式从列表右/左侧取出消息。
  • 特点:
    • 简单高效,支持阻塞等待。
    • 消息只能被消费一次,无确认机制(消息取出后即删除,若处理失败会丢失)。
  • 示例:
# 生产者发送消息
LPUSH my_queue "message1"

# 消费者阻塞等待消息(超时时间10秒)
BRPOP my_queue 10

2. 发布订阅模式(Pub/Sub)
  • 核心命令:
    • PUBLISH:发布消息到频道。
    • SUBSCRIBE:订阅频道接收消息。
  • 特点:
    • 支持广播,实时性高。
    • 消息无持久化(订阅者离线时丢失消息),无法回溯历史消息。
  • 示例:
# 订阅者订阅频道
SUBSCRIBE my_channel

# 生产者发布消息
PUBLISH my_channel "Hello, World!"

3. 基于Streams的可靠队列(推荐)

Redis 5.0+ 支持 Streams 数据类型,提供更完整的消息队列功能:

  • 消息持久化:消息存储在 Stream 中,可重复读取。

  • 消费者组(Consumer Group):支持多消费者协同处理,消息确认机制(ACK)。

  • Pending Entries List (PEL):跟踪处理中的消息,避免丢失。

  • 核心命令:
    • ​​​​​​​XADD:添加消息到 Stream。
    • XREADGROUP:消费者组读取消息。
    • XACK:确认消息处理完成。
  • 示例:​​​​​​​
# 创建消息流
XADD mystream * field1 "value1" field2 "value2"

# 创建消费者组
XGROUP CREATE mystream mygroup 0

# 消费者从组内读取消息(阻塞模式)
XREADGROUP GROUP mygroup consumer1 BLOCK 1000 COUNT 1 STREAMS mystream >

# 处理完成后确认消息
XACK mystream mygroup 1629164893000-0

3.1 单消费者模式(Single Consumer)
  • 定义:单个消费者直接从一个 Stream 中读取消息,每条消息只能被消费一次。
  • 特点:
    • ​​​​​​​独立消费:消费者通过 XREAD 命令主动拉取消息。
    • 无状态管理:需手动记录已处理消息的ID,避免重复消费。
    • 简单轻量:适合单实例、低并发的场景。
  • 潜在问题:
    • ​​​​​​​消息重复消费:若消费者崩塌,重启后需手动记录上次处理的位置(通过LASTID)
    • 无负载均衡:无法通过多实例提升处理能力。
  • 示例:
# 生产者发送消息到 Stream
XADD mystream * field1 "value1" field2 "value2"

# 消费者读取消息(阻塞式,最多读取2条)
XREAD COUNT 2 BLOCK 5000 STREAMS mystream 0
# 输出:
# 1) 1) "mystream"
#    2) 1) 1) "1678459545000-0"
#          2) 1) "field1" 2) "value1" 3) "field2" 4) "value2"

3.2 消费者组模式(Consumer Group)
  • 定义:多个消费者组成一个逻辑组,协同消费同一个 Stream 中的消息。每条消息仅被组内一个消费者处理。
  • 核心机制:
    • ​​​​​​​消息分配:Redis 自动将消息分发给组内不同的消费者。
    • 故障转移:若某个消费者宕机,未确认(Pending)的消息会被重新分配给其他消费者。
    • 消息确认(ACK):消费者处理完成后需发送ACK,否则消息会重新入队。
  • 核心优势:
    • ​​​​​​​负载均衡:组内多个消费者并行处理消息,提升吞吐量。
    • 消息可靠性:通过 ACK 机制和 Pending Entries List(PEL)确保消息必达。
    • 自动重试:未确认的消息会被重新分配给其他消费者。
  • 示例:
# 创建消费者组(关联到 Stream)
XGROUP CREATE mystream mygroup 0

# 消费者加入组并消费消息(消费者1)
XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 5000 STREAMS mystream >
# 输出:
# 1) 1) "mystream"
#    2) 1) 1) "1678459545000-0"
#          2) 1) "field1" 2) "value1" 3) "field2" 4) "value2"

# 处理完成后发送ACK确认
XACK mystream mygroup 1678459545000-0

# 另一个消费者加入组(消费者2)
XREADGROUP GROUP mygroup consumer2 COUNT 1 BLOCK 5000 STREAMS mystream >
消费者组配置示例
# 创建消费者组(若Stream不存在则自动创建)
XGROUP CREATE mystream mygroup $ MKSTREAM

# 消费者1加入组并消费消息
XREADGROUP GROUP mygroup consumer1 COUNT 10 BLOCK 5000 STREAMS mystream >

# 消费者2加入组并消费消息
XREADGROUP GROUP mygroup consumer2 COUNT 10 BLOCK 5000 STREAMS mystream >

# 查看未确认的消息(Pending List)
XPENDING mystream mygroup

实现业务:

修改代码:
1. 在redis中创建消息队列
# 创建队列(消费者组模式)
XGROUP CREATE stream.orders g1 0 MKSTREAM
2. 修改lua脚本
-- 1.参数列表
-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]
-- 订单id
local orderId = ARGV[3]

-- 2.数据key
-- 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务逻辑
-- 判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 库存不足,返回1
    return 1
end
-- 判断用户是否已下单  SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 用户重复下单,返回2
    return 2
end

-- 扣减库存,并判断扣减是否成功 INCRBY stockKey -1
redis.call("incrby", stockKey, -1)
-- 下单,保存用户id到集合 SADD orderKey userId
redis.call("sadd", orderKey, userId)
-- 发送消息到队列中 XADD stream.orders * k1 v1 k2 v2...
redis.call("xadd", "stream.orders", "*", "userId", userId, "voucherId", voucherId, "id", orderId)
return 0
3.修改业务代码,实现消息队列异步优化
/**
     * 优惠券订单任务处理线程:不断从消息队列中获取消息,并创建订单
     */
    private class VoucherOrderHandler implements Runnable {

        /**
         * 队列名称
         */
        String queueName = "stream.orders";

        @Override
        public void run() {
            while (true) {
                try {
                    //1.获取消息队列中的订单信息  XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >(‘>’代表未消费)
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    //2.判断消息获取是否成功
                    if (list == null || list.isEmpty()) {
                        //2.1如果获取失败,说明没有消息,继续下一次循环获取消息
                        continue;
                    }
                    //3.如果获取成功,解析消息中的订单信息
                    //将消息转成VoucherOrder对象
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> values = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
                    //3.1 创建订单完成下单
                    handleVoucherOrder(voucherOrder);
                    //4.ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
                } catch (Exception e) {
                    //处理异常消息
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

        /**
         * 处理pending-list中的消息
         */
        private void handlePendingList() {
            while (true) {
                try {
                    //1.获取pending-list中的订单信息  XREADGROUP GROUP g1 c1 COUNT 1 STREAMS stream.orders 0('0'代表已消费但未确认)
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create(queueName, ReadOffset.from("0"))
                    );
                    //2.判断消息获取是否成功
                    if (list == null || list.isEmpty()) {
                        //2.1如果获取失败,说明pending-list中没有异常消息,直接结束循环
                        break;
                    }
                    //3.如果获取成功,解析消息中的订单信息
                    //将消息转成VoucherOrder对象
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> values = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
                    //3.1 创建订单完成下单
                    handleVoucherOrder(voucherOrder);
                    //4.ACK确认  SACK stream.orders g1 id
                    stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
                } catch (Exception e) {
                    //处理异常消息
                    log.error("处理pending-list中的订单信息失败,{}", e.getMessage());
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }

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

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

相关文章

JavaScript 的演变:2023-2025 年的新特性解析

随着Web技术的飞速发展&#xff0c;ECMAScript&#xff08;简称ES&#xff09;作为JavaScript的语言标准&#xff0c;也在不断进化。 本文将带你学习 ECMAScript 2023-2025 的新特性。 一、ECMAScript 2023 新特性 1.1 数组的扩展 Array.prototype.findLast()/Array.protot…

[Java · 初窥门径] Java 注释符

&#x1f31f; 想系统化学习 Java 编程&#xff1f;看看这个&#xff1a;[编程基础] Java 学习手册 0x01&#xff1a;Java 注释符简介 在编写程序时&#xff0c;为了使代码易于理解&#xff0c;通常会为代码加一些注释。Java 注释就是用通俗易懂的语言对代码进行描述或解释&a…

Docker环境下SpringBoot程序内存溢出(OOM)问题深度解析与实战调优

文章目录 一、问题背景与现象还原**1. 业务背景****2. 故障特征****3. 核心痛点****4. 解决目标** 二、核心矛盾点分析**1. JVM 与容器内存协同失效****2. 非堆内存泄漏****3. 容器内存分配策略缺陷** 三、系统性解决方案**1. Docker 容器配置**2. JVM参数优化&#xff08;容器…

【计算机网络】网络基础(协议,网络传输流程、Mac/IP地址 、端口号)

目录 1.协议简述2.网络分层结构2.1 软件分层2.2 网络分层为什么&#xff1f; 是什么&#xff1f;OSI七层模型TCP/IP五层&#xff08;或四层&#xff09;结构 3. 网络与操作系统之间的关系4.从语言角度理解协议5.网络如何传输局域网通信&#xff08;同一网段&#xff09; 不同网…

pgsql中使用jsonb的mybatis-plus和jps的配置

在pgsql中使用jsonb类型的数据时&#xff0c;实体对象要对其进行一些相关的配置&#xff0c;而mybatis和jpa中使用各不相同。 在项目中经常会结合 MyBatis-Plus 和 JPA 进行开发&#xff0c;MyBatis_plus对于操作数据更灵活&#xff0c;jpa可以自动建表&#xff0c;两者各取其…

使用MetaGPT 创建智能体(2)多智能体

先给上个文章使用MetaGPT 创建智能体&#xff08;1&#xff09;入门打个补丁&#xff1a; 补丁1&#xff1a; MeteGTP中Role和Action的关联和区别&#xff1f;这是这两天再使用MetaGPT时候心中的疑问&#xff0c;这里做个记录 Role&#xff08;角色&#xff09;和 Action&…

C# 使用.NET内置的 IObservable<T> 和 IObserver<T>-观察者模式

核心概念 IObservable<T> 表示 可观察的数据源&#xff08;如事件流、实时数据&#xff09;。 关键方法&#xff1a;Subscribe(IObserver<T> observer)&#xff0c;用于注册观察者。 IObserver<T> 表示 数据的接收者&#xff0c;响应数据变化。 三个核心…

Redis——网络模型之IO讲解

目录 前言 1.用户空间和内核空间 1.2用户空间和内核空间的切换 1.3切换过程 2.阻塞IO 3.非阻塞IO 4.IO多路复用 4.1.IO多路复用过程 4.2.IO多路复用监听方式 4.3.IO多路复用-select 4.4.IO多路复用-poll 4.5.IO多路复用-epoll 4.6.select poll epoll总结 4.7.IO多…

vue3 传参 传入变量名

背景&#xff1a; 需求是&#xff1a;在vue框架中&#xff0c;接口传参我们需要穿“变量名”&#xff0c;而不是字符串 通俗点说法是&#xff1a;在网络接口请求的时候&#xff0c;要传属性名 效果展示&#xff1a; vue2核心代码&#xff1a; this[_keyParam] vue3核心代码&…

旅游特种兵迪士尼大作战:DeepSeek高精准路径优化

DeepSeek大模型高性能核心技术与多模态融合开发 - 商品搜索 - 京东 随着假期的脚步日渐临近&#xff0c;环球影城等备受瞩目的主题游乐场&#xff0c;已然成为大人与孩子们心中不可或缺的节日狂欢圣地。然而&#xff0c;随之而来的庞大客流&#xff0c;却总让无数游客在欢乐的…

【MySQL】第一弹——MySQL数据库结构与操作

目录 一. 数据库介绍1.1 什么是数据库1.2 为什么要使用数据库1.3 主流数据库1.3.1 关系型数据库1.3.2 非关系型数据库 二. MySQL 的结构2.1 MySQL服务器和客户端2.2 MySQL服务器是如何组织数据的 三. 数据库的操作3.1 创建数据库语法格式示例 3.2 查看数据库语法格式示例 3.3 使…

Spring_MVC 快速入门指南

Spring_MVC 快速入门指南 一、Spring_MVC 简介 1. 什么是 Spring_MVC&#xff1f; Spring_MVC 是 Spring 框架的一个模块&#xff0c;用于构建 Web 应用程序。它基于 MVC&#xff08;Model-View-Controller&#xff09;设计模式&#xff0c;将应用程序分为模型&#xff08;M…

L38.【LeetCode题解】四数之和(双指针思想) 从汇编角度分析报错原因

目录 1.题目 2.分析 去重的代码 错误代码 3.完整代码 提交结果 1.题目 四数之和 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元…

字符串系列一>最长回文子串

目录 题目&#xff1a;解析&#xff1a;代码&#xff1a; 题目&#xff1a; 链接: link 解析&#xff1a; 代码&#xff1a; class Solution {public String longestPalindrome(String s) {char[] ss s.toCharArray();int n ss.length;int begin 0;//返回结果的起始字符串…

双击热备方案及不同方案的需求、方案对比

双击热备方案概述 一般实现双机热备的方案有三种,分别是共享存储双机热备方案、镜像双机热备方案、双机双柜双机热备方案,这三种方案对硬件要求不同,大家可以根据自身的业务应用特性来选择具体的双机热备方案以及对应的ServHA双机热备软件产品。 1、镜像双机热备 1.1镜像…

VSCODE插值表达式失效问题

GET https://cdn.jsdelivr.net/npm/vue2.6.14/dist/vue.js net::ERR_CONNECTION_-CSDN博客 更换正确的vue域名 GET https://cdn.jsdelivr.net/npm/vue2.6.14/dist/vue.js net::ERR_CONNECTION_ <script src"https://unpkg.com/vue2.6.14/dist/vue.js"></sc…

【失败】Gnome将默认终端设置为 Kitty

起因 一会儿gnome-terminal一会儿kitty终端&#xff0c;实在是受不了&#xff0c;决定取缔默认的gnome-terminal。 过程 在 Ubuntu 或 Debian 系统上&#xff1a; 确保 Kitty 已经安装。如果未安装&#xff0c;可以在终端中运行命令sudo apt install kitty -y进行安装。 使用系…

蓝桥杯:连连看

本题大意要我们在一个给定的nxm的矩形数组中找出符合条件的格子 条件如下&#xff1a; 1.数值相同 2.两个横坐标和纵坐标的差值相等&#xff08;由此可得是一个对角线上的格子&#xff09; 那么根据以上条件我们可以用HashMap来解决这个问题&#xff0c;统计对角线上数值相同…

Ext系列⽂件系统

Ext系列⽂件系统 1. 理解硬件1.1 磁盘的物理结构1.2 磁盘的存储结构1.3 磁盘的逻辑结构理解过程实际过程 1.4 CHS&&LBA地址 2. 引入文件系统块分区innode 3. Ext2文件系统3.1 宏观认识3.2 block group3.3 块组内部3.3.1 GDT&#xff08;Group Descriptor Table&#xf…

MTK-Android12 13 屏蔽掉Viewing full screen

去掉ROOM 开机第一次提示全屏弹框 文章目录 需求参考资料修改文件实现方案 解决思路grep 源码查找信息grep 查找 grep -rn "Viewing full screen" 找string 字段grep 查找 grep -rn immersive_cling_title 布局grep 查找 grep -rn layout.immersive_mode_cling 对应的…