Redis从入门到精通(九)Redis实战(六)基于Redis队列实现异步秒杀下单

文章目录

    • 前言
    • 4.5 分布式锁-Redisson
      • 4.5.4 Redission锁重试
      • 4.5.5 WatchDog机制
      • 4.5.5 MutiLock原理
    • 4.6 秒杀优化
      • 4.6.1 优化方案
      • 4.6.2 完成秒杀优化
    • 4.7 Redis消息队列
      • 4.7.1 基于List实现消息队列
      • 4.7.2 基于PubSub的消息队列
      • 4.7.3 基于Stream的消息队列
      • 4.7.4 基于Stream的消息队列-消费者组
      • 4.7.5 基于Stream的消息队列实现异步秒杀下单

前言

Redis实战系列文章:

Redis从入门到精通(四)Redis实战(一)短信登录
Redis从入门到精通(五)Redis实战(二)商户查询缓存
Redis从入门到精通(六)Redis实战(三)优惠券秒杀
Redis从入门到精通(七)Redis实战(四)库存超卖、一人一单与Redis分布式锁
Redis从入门到精通(八)Redis实战(五)分布式锁误删与原子性问题、Redisson

4.5 分布式锁-Redisson

上一节对Redisson进行了快速入门,并分析了可重入锁的基本原理,下面继续研究一些Redisson的几个功能。

4.5.4 Redission锁重试

// org.redisson.RedissonLock#lock()

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 返回null表示获取锁成功,否则返回锁的剩余生存时间
if (ttl == null) {
    return;
}

// ......

// 重试获取锁
while (true) {
    ttl = tryAcquire(-1, leaseTime, unit, threadId);
    if (ttl == null) {
        break;
    }
    // ......
}

由以上源码可知,在RedissonLock类的lock()方法中,会调用tryAcquire()方法尝试获取锁。tryAcquire()方法的原理在上一节已经分析过,返回null表示获取锁成功,否则返回锁的剩余生存时间。

如果第一次获取锁失败,程序会进入一个while循环,重试获取锁。

4.5.5 WatchDog机制

// org.redisson.RedissonLock

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 调用Lua脚本
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
        // 执行看门狗机制
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

private void scheduleExpirationRenewal(long threadId) {
    // ......
    } else {
        entry.addThreadId(threadId);
        // 执行看门狗
        renewExpiration();
    }
}

private void renewExpiration() {
    // ......
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // ......
            // 调用Lua脚本刷新锁的有效时间
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    // logger
                    return;
                }
                if (res) {
                    // 递归执行看门狗
                    renewExpiration();
                }
            });
        }
    // 10s执行一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    ee.setTimeout(task);
}

由以上源码可知,在RedissonLock类的tryAcquireAsync()方法中,除了调用Lua脚本获取锁,还会运行看门狗机制。该机制会调用Lua脚本刷新锁的有效时间,同时每10s递归执行一次看门狗。

4.5.5 MutiLock原理

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

以主从为例,此时要去获取锁,命令写在主机上,主机会将数据同步给从机。假设在主机还没有来得及把数据写入到从机去的时候,主机宕机了,哨兵会发现主机宕机,并且选举一个Slave变成Master,但此时新的Master中实际上并没有锁信息,相当于此时锁信息已经丢掉了。

为了解决这个问题,Redission提出来了MutiLock锁,使用这种锁后每个节点的地位都是一样的,加锁的逻辑需要把数据写入到每一个主丛节点上,只有所有的节点都写入成功,此时才是真的加锁成功。

假设现在某个节点挂了,那么去获得锁的时候,有一个节点拿不到,不能算是加锁成功,就保证了加锁的可靠性。

4.6 秒杀优化

4.6.1 优化方案

  • 现存问题

如上图所示,秒杀下单包括六个步骤:查询优惠券、判断秒杀库存、查询订单、校验一人一单、减库存、创建订单。

在这六步操作中,有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致程序执行的很慢。 那么如何加速呢?

  • 优化方案

把简单的校验(例如是否有库存、是否一人一单)做完后,就直接给用户返回成功或失败,而不必等待订单创建完成。如果确定可以下单,则将订单的相关信息写入队列,然后再创建一个线程,让新线程读取队列信息异步进行下单。 如下图所示:

  • 整体思路

当用户下单时,首先通过Redis判断库存是否充足,如果不充足则直接返回失败;充足的话,再通过Redis判断用户是否已经下过单,如果已经下过单,则直接返回失败;如果没有下过单,则说明可以下单,进行库存扣减,并将用户ID存入当前优惠券的集合中。由于以上过程需要保证原子性,因此可以通过Lua脚本来完成。可以成功下单,Lua脚本返回0。

接着判断Lua脚本的执行结果。如果Lua脚本返回0,说明可以下单,则将优惠券ID、用户ID和订单ID存入阻塞队列,并返回订单ID给用户;如果Lua脚本没有返回0,则直接返回错误信息给用户。

最后进行异步下单,即通过额外线程读取阻塞队列的信息并真正进行下单。完整的流程如下图所示。

4.6.2 完成秒杀优化

  • 需求1:新增秒杀优惠券的同时,将优惠券信息保存到Redis中
// com.star.redis.dzdp.service.impl.VoucherServiceImpl

@Override
public BaseResult addSeckillVoucher(Voucher voucher) {
    log.info("add a seckill voucher, {}", voucher.toString());
    // 1.保存优惠券信息
    save(voucher);
    log.info("add voucher success. id = {}", voucher.getId());
    // 2.保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 3.将秒杀优惠券的库存保存到Redis
    String key = "seckill:stock:" + voucher.getId();
    stringRedisTemplate.opsForValue().set(key, voucher.getStock().toString());
    log.info("set to Redis : Key = {}, Value = {}", key, voucher.getStock().toString());
    return BaseResult.setOk("新增秒杀券成功!");
}

调用/voucher/seckill/order接口新增一个描述优惠券:

在Redis中可以看到该秒杀优惠券的库存信息:

  • 需求2:基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

在resources目录下新建一个order.lua文件,其内容如下:

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

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

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
  • 需求3:如果抢购成功,将优惠券ID、用户ID和订单ID封装后存入阻塞队列

修改VoucherOrderServiceImpl类的下单方法seckillVoucher()

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

/** 保存订单信息的队列 */
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

@Override
public BaseResult<Long> seckillVoucher(Long voucherId, Long userId) {
    log.info("开始秒杀下单...voucherId = {}, userId = {}", voucherId, userId);
    Long orderId = RedisIdWorker.nextId(stringRedisTemplate, "voucher_order");
    log.info("get orderId = {}", orderId);
    // 1.执行Lua脚本
    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
    script.setLocation(new ClassPathResource("order.lua"));
    script.setResultType(Long.class);
    Long result = stringRedisTemplate.execute(script, Collections.emptyList(),
            voucherId.toString(), userId.toString(), orderId.toString());
    log.info("execute order.lua result = {}", result);
    // 2.判断执行结果
    if(result == null || result != 0) {
        // 结果为空或者不为0
        String message = (result == null || result == 1) ? "库存不足" : "不能重复下单";
        log.error(message);
        return BaseResult.setFail(message);
    }
    // 3.结果为0,将优惠券ID、用户ID和订单ID封装后存入阻塞队列
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setVoucherId(voucherId);
    voucherOrder.setUserId(userId);
    voucherOrder.setId(orderId);
    orderTasks.add(voucherOrder);
    log.info("add voucherId = {}, userId = {}, orderId = {} to queue.. done.",
            voucherId, userId, orderId);
    // 4.返回订单ID
    log.info("秒杀下单返回...orderId = {}", orderId);
    return BaseResult.setOkWithData(orderId);
}
  • 需求4:开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

/** 异步执行下单动作的线程池 */
private static final ExecutorService SECKILL_ORDER_EXECUTOR =
        Executors.newSingleThreadExecutor();

/** 类初始化之后立即初始化线程池 */
@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

/**
 * 处理订单的内部类
 */
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        // while循环持续读取队列中的信息
        while (true) {
            try {
                log.info("=====begin=====>");
                // 1.获取队列中的订单信息
                VoucherOrder voucherOrder = orderTasks.take();
                log.info("get from queue : {}", voucherOrder.toString());
                // 2.创建订单
                handleVoucherOrder(voucherOrder);
                log.info("=====end=====>");
            } catch (Exception e) {
                log.error("处理异常订单", e);
            }
        }
    }

    /**
     * 处理订单
     */
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        // 1.创建锁对象
        RLock rLock = redissonClient.getLock("lock:order:" + voucherOrder.getUserId());
        // 2.尝试获取锁
        boolean isLock = rLock.tryLock();
        log.info("isLock = {}", isLock);
        // 3.判断是否获取锁成功
        if(!isLock) {
            // 获取锁失败
            log.error("不允许重复下单!");
            return;
        }
        try {
            // 4.持锁真正创建订单
            checkAndCreateVoucherOrder(voucherOrder.getVoucherId(), voucherOrder.getUserId());
        } finally {
            // 5.释放锁
            rLock.unlock();
            log.info("unlock done.");
        }
    }
    
    /**
     * 持锁真正创建订单
     */
    private void createVoucherOrder(VoucherOrder voucherOrder) {
        log.info("begin createVoucherOrder... voucherId = {}, userId = {}, orderId = {}",
                voucherOrder.getVoucherId(), voucherOrder.getUserId(), voucherOrder.getId());
        // 1.增加一人一单规则
        int count = query().eq("voucher_id", voucherOrder.getVoucherId())
                .eq("user_id", voucherOrder.getUserId()).count();
        log.info("old order count = {}", count);
        if(count > 0) {
            // 该用户已下过单
            log.error("每个帐号只能抢购一张优惠券!");
            return;
        }
        // 2.扣减库存
        boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId())
                .gt("stock", 0)
                .update();
        log.info("update result = {}", update);
        if(!update) {
            // 扣减库存失败,返回抢券失败
            log.error("库存不足,抢券失败!");
            return;
        }
        // 3.创建订单
        voucherOrder.setPayTime(new Date());
        voucherOrderService.save(voucherOrder);
    }
}

下面借助工具对秒杀下单接口进行性能测试,结果如下:

由于使用的是同一用户,因此971个请求中,只有一个请求是成功的,其余的请求都失败。查看此时Redis中的订单数据,只有1条:

4.7 Redis消息队列

如上图所示,最简单的消息队列包含3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker);
  • 生产者:发送消息到消息队列;
  • 消费者:从消息队列获取消息并处理消息。

使用队列的好处在于解耦。 在秒杀下单中,用户下单之后,利用Redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快了响应速度。

4.7.1 基于List实现消息队列

Redis的List数据结构是一个双向链表,很容易模拟出队列效果。我们可以利用:LPUSH结合RPOP、或者RPUSH结合LPOP来实现。

不过要注意的是,当队列中没有消息时,RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。如图:

基于List的消息队列有哪些优缺点?

优点:

  • 利用Redis存储,不受限于JVM内存上限;
  • 基于Redis的持久化机制,数据安全性有保证;
  • 可以满足消息有序性。

缺点:

  • 无法避免消息丢失;
  • 只支持单消费者。

4.7.2 基于PubSub的消息队列

PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。如图:

主要命令有:

# 订阅一个或多个频道
SUBSCRIBE channel [channel]
# 订阅与pattern格式匹配的所有频道
PSUBSCRIBE pattern[pattern]
# 向一个频道发送消息
PUBLISH channel msg

基于PubSub的消息队列有哪些优缺点?

优点:

  • 采用发布订阅模型,支持多生产、多消费。

缺点:

  • 不支持数据持久化;
  • 无法避免消息丢失;
  • 消息堆积有上限,超出时数据丢失。

4.7.3 基于Stream的消息队列

Stream是Redis 5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列。

发送消息的命令是:

例如:

127.0.0.1:6379> XADD users * name Rose age 22
"1712458704764-0"
127.0.0.1:6379> XADD users * name Jack age 30
"1712458778623-0"

读取消息的方式之一:XREAD

例如,使用XREAD读取第一个消息:

127.0.0.1:6379> XREAD COUNT 1 STREAMS users 0
1) 1) "users"
   2) 1) 1) "1712458704764-0"
         2) 1) "name"
            2) "Rose"
            3) "age"
            4) "22"

XREAD阻塞方式,读取最新消息:

# 阻塞1秒
127.0.0.1:6379> XREAD COUNT 1 BLOCK 1000 STREAMS users $
(nil)
(1.02s)

基于STREAM的消息队列的特点:

  • 消息可回溯;
  • 一个消息可以被多个消费者读取;
  • 可以阻塞读取;
  • 有消息漏读的风险。

4.7.4 基于Stream的消息队列-消费者组

消费者组(Consumer Group),就是将多个消费者划分到一个组中,监听同一个队列。它具备下列特点:

创建消费者组:

127.0.0.1:6379> XGROUP CREATE users a_group 0
OK

给自定的消费者组添加消费者:

127.0.0.1:6379> XGROUP CREATECONSUMER users a_group a_consumer1
(integer) 1

从消费者组读取消息:

127.0.0.1:6379> XREADGROUP GROUP a_group a_consumer1 COUNT 1 STREAMS users 0
1) 1) "users"
   2) (empty array)

基于STREAM消费者组的消息队列的特点:

  • 消息可回溯;
  • 可以多消费者争抢消息,加快消费速度;
  • 可以阻塞读取;
  • 没有消息漏读的风险;
  • 有消息确认机制,保证消息至少被消费一次。

下面,对比一下这4种消息队列的特点:

经过比较,本案例选择使用基于Stream的消息队列来实现异步秒杀下单。

4.7.5 基于Stream的消息队列实现异步秒杀下单

  • 修改秒杀下单Lua脚本order.lua,在认定有抢购资格后,直接向stream.orders队列中添加消息,内容包含voucherId、userId、orderId
-- ...

-- 新增逻辑
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)

return 0
  • 修改消息读取策略,改为读取Redis的Stream结构队列
// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl#seckillVoucher()

// ......

// 3.结果为0,将优惠券ID、用户ID和订单ID封装后存入阻塞队列
// 新逻辑:这里不再保存队列,在lua脚本中保存
// VoucherOrder voucherOrder = new VoucherOrder();
// voucherOrder.setVoucherId(voucherId);
// voucherOrder.setUserId(userId);
// voucherOrder.setId(orderId);
// orderTasks.add(voucherOrder);
// log.info("add voucherId = {}, userId = {}, orderId = {} to queue.. done.",
//         voucherId, userId, orderId);

// 4.返回订单ID
log.info("秒杀下单返回...orderId = {}", orderId);
return BaseResult.setOkWithData(orderId);
// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        // 持续读取队列中的信息
        while (true) {
            try {
                log.info("=====begin=====>");
                // 1.获取队列中的订单信息
                // VoucherOrder voucherOrder = orderTasks.take();
                // log.info("get from queue : {}", voucherOrder.toString());

                // 1.新逻辑:读取Redis的Stream消息队列
                // XREADGROUP GROUP a_group a_consumer1 COUNT 1 BLOCK 2000 STREAMS stream.orders >
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("a_group", "a_consumer1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));
                // 2.判断订单信息是否为空
                if(list == null || list.isEmpty()) {
                    // 如果为空,说明没有消息,继续下一次循环
                    continue;
                }
                // 3.解析数据
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                log.info("get from Redis Stream queue : id = {}, {}", record.getId(), voucherOrder.toString());

                // 4.创建订单
                handleVoucherOrder(voucherOrder);

                // 5.确认消息
                stringRedisTemplate.opsForStream().acknowledge("stream.orders", "a_group", record.getId());
                log.info("ack message done.");

                log.info("=====end=====>");
            } catch (Exception e) {
                log.error("处理异常订单", e);
            }
        }
    }
    
    // ......
}

测试:

[http-nio-8081-exec-2] 开始秒杀下单...voucherId = 15, userId = 1012
[http-nio-8081-exec-2] get orderId = 7354966481756487681
[http-nio-8081-exec-2] execute order.lua result = 0
[http-nio-8081-exec-2] add voucherId = 15, userId = 1012, orderId = 7354966481756487681 to queue.. done.
[http-nio-8081-exec-2] 秒杀下单返回...orderId = 7354966481756487681
// 创建新线程异步处理下单逻辑
// 成功获取到Stream队列的消息
[pool-2-thread-1] get from Redis Stream queue : id = 1712461578801-0, VoucherOrder(id=7354966481756487681, userId=1012, voucherId=15, payType=null, status=null, createTime=null, payTime=null, useTime=null, refundTime=null, updateTime=null)
[pool-2-thread-1] isLock = true
[pool-2-thread-1] begin createVoucherOrder... voucherId = 15, userId = 1012, orderId = 7354966481756487681
[pool-2-thread-1] ==>  Preparing: SELECT COUNT( * ) FROM tb_voucher_order WHERE (voucher_id = ? AND user_id = ?)
[pool-2-thread-1] ==> Parameters: 15(Long), 1012(Long)
[pool-2-thread-1] <==      Total: 1
[pool-2-thread-1] old order count = 0
[pool-2-thread-1] ==>  Preparing: UPDATE tb_seckill_voucher SET stock = stock - 1 WHERE (voucher_id = ? AND stock > ?)
[pool-2-thread-1] ==> Parameters: 15(Long), 0(Integer)
[pool-2-thread-1] <==    Updates: 1
[pool-2-thread-1] update result = true
[pool-2-thread-1] ==>  Preparing: INSERT INTO tb_voucher_order ( id, user_id, voucher_id, pay_time ) VALUES ( ?, ?, ?, ? )
[pool-2-thread-1] ==> Parameters: 7354966481756487681(Long), 1012(Long), 15(Long), 2024-04-07 11:46:21.208(Timestamp)
[pool-2-thread-1] <==    Updates: 1
[pool-2-thread-1] unlock done.
// 消息确认完成
[pool-2-thread-1] ack message done.

可见,基于Stream的消息队列正常工作。

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DIY可视化UniApp表格组件

表格组件在移动端的用处非常广泛&#xff0c;特别是在那些需要展示结构化数据、进行比较分析或提供详细信息的场景中。数据展示与整理&#xff1a;表格是展示结构化数据的理想方式&#xff0c;特别是在需要展示多列和多行数据时。通过表格&#xff0c;用户可以轻松浏览和理解数…

Spring Boot-02-依赖管理和自动配置

二、Spring Boot的两大重要机制 1. 依赖管理机制 开发什么场景&#xff0c;导入什么场景启动器&#xff0c;场景启动器自动把这个场景的所有核心依赖全部导入进来。maven依赖传递原则&#xff1a;A依赖B&#xff0c;B依赖C&#xff0c;则A就拥有B和C。每个boot项目都有一个父…

防火墙配置IPSec VPN【IPSecVPN概念及详细讲解】

防火墙配置IPSecVPN-点到点 配置目标 总公司内网与分公司内网互通 拓扑 配置ISP路由器 <Huawei>u t m <Huawei>sys [Huawei]sys ISP [ISP]interface g0/0/0 [ISP-GigabitEthernet0/0/0]ip address 100.1.1.102 24 [ISP-GigabitEthernet0/0/0]q [ISP]interface g…

【cocos creator】【编辑器插件】cocos creator文件复制时,解决cocos creator uuid冲突

&#xff01;&#xff01;&#xff01;修改前先备份 1、将文件夹放在packages文件夹下 2、打开项目&#xff0c;选择要刷新uuid的文件夹 3、菜单栏点击 扩展->refresh-uuid 4、等控制台提示&#xff1a;资源uuid刷新完成&#xff0c;重启项目&#xff08;&#xff01;&#…

VSCODE自动更新无法连接远程服务器报错“waiting for server log...“的解决方法

问题描述 一觉醒来打开vscode发现连接远程服务器显示无法连接&#xff0c;终端一直报错“waiting for server log…"&#xff0c;经查是因为vscode自动更新到了1.86&#xff0c;对于远程服务器的linux版本要求较高。这里记录下解决方法。 解决方法 1. 下载vscode便携版…

网络安全之命令注入

漏洞原理&#xff1a; 应用系统设计需要给用户提供指定的远程命令操作的接口&#xff0c;比如&#xff1a;路由器&#xff0c;防火墙&#xff0c;入侵检测等设备的web管理界面。一般会给用户提供一个ping操作的web界面 用户从web界面输入目标IP&#xff0c;提交后台会对改IP地…

Web 前端性能优化之五:构建优化

4、构建优化 资源的合并与压缩所涉及的优化点包括两方面&#xff1a;一方面是减少HTTP的请求数量&#xff0c;另一方面是减少HTTP请求资源的大小。 1、HTML 压缩 1、什么是 HTML 压缩 百度首页部分 HTML 源代码 谷歌首页部分 HTML 源代码 虽然这些格式化的字符能带来很好的代…

LiDAR点云转3D Tiles

到 2026年&#xff0c;一个国家项目旨在绘制整个法国领土的三维地图。 美国国家地理和林业信息研究所 (IGN) 是这个名为“Lidar HD”的“项目”的幕后黑手&#xff0c;其目标是获得该地区非常精确的 3D 描述。 NSDT工具推荐&#xff1a; Three.js AI纹理开发包 - YOLO合成数据生…

内外网数据交换发展进程:安全与便捷并行

随着信息化的不断推进&#xff0c;医院、党政以及企业的内外网数据交换正成为日益关注的焦点。在保障数据安全的前提下&#xff0c;需要寻求一种既安全可靠又操作便捷的数据传输方式。本文将探讨内外网数据交换发展进程&#xff0c;分析各种传输方式的优缺点&#xff0c;以及它…

爬虫入狱笔记——xx政府网站公开政策数据

最近在学习爬虫&#xff0c;做个笔记吧 今天爬xx政府网站-政策法规栏目的数据 咱们首先需要找到数据从哪里来&#xff0c;鼠标右键->检查&#xff08;或者快捷键一般为F12&#xff09;检查元素&#xff0c;搜索关键词 eg.【违法案例】 回车&#xff0c; 如果没有的话&am…

【随笔】Git 高级篇 -- 整理提交记录(下)rebase(十六)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

【Vue】我的第一个组件

文章目录 项目简介 项目简介 项目根目录中的index.html是项目的入口文件 加载index.html&#xff0c;vite解析。指向的src下的ts文件或者js文件 最后通过vue3的createApp函数创建一个应用&#xff0c;并挂载到指定div下 App.vue结构说明 特别注意:script脚本内&#xff0…

51单片机入门_江协科技_17~18_OB记录的笔记

17. 定时器 17.1. 定时器介绍&#xff1a;51单片机的定时器属于单片机的内部资源&#xff0c;其电路的连接和运转均在单片机内部完成&#xff0c;无需占用CPU外围IO接口&#xff1b; 定时器作用&#xff1a; &#xff08;1&#xff09;用于计时系统&#xff0c;可实现软件计时&…

Golang 开发实战day08 - Multiple Return values

Golang 教程08 - Multiple Return values 1. Multiple return values 1.1 如何理解多个返回值&#xff1f; Go语言中的多返回值&#xff0c;就像你听了一首歌曲yellow&#xff0c;可以从歌曲里反馈出忧郁和害羞&#xff01;Goland的多个返回值就类似于如此&#xff0c;设定一…

C++版本GDAL3.5无法找到proj.db文件

问题&#xff1a;C版本的GDAL无法找到proj.db文件 自己编译过的gdal3.5版本在自己电脑上使用坐标转换没有问题&#xff0c;而将库文件和头文件迁移到别的笔记本上转换坐标出实现问题&#xff1a; ERROR 1: PROJ: proj_create_from_database: Cannot find proj.db ERROR 1: PRO…

C语言第四十一弹---猜数字游戏

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 猜数字游戏 1、随机数生成 1.1、rand 1.2、srand 1.3、time 1.4、设置随机数的范围 2、猜数字游戏的分析和设计 2.1、猜数字游戏功能说明 2.2、猜数字游戏…

新人硬件工程师往哪个方向更有前途?

如果是比较沉默寡言&#xff0c;不擅长交际的&#xff0c;那么可以走技术路线。我这里有一套自动化入门教程&#xff0c;不仅包含了详细的视频讲解&#xff0c;项目实战。如果你渴望学习自动化&#xff0c;不妨点个关注&#xff0c;给个评论222&#xff0c;私信22&#xff0c;我…

让chatGPT控制物理设备

作为自动控制行业的工程师&#xff0c;我们也许最关心的是如何使chatGPT 控制物理设备。我发现许多人仍然停留在传统程序设计的思维阶段&#xff0c;比如让大模型编写一段PLC 代码&#xff0c;或者是生成一些信息模型。 其实大模型具备判断与思考的能力&#xff0c;AI …

数字乡村:科技引领新时代农村发展

随着信息技术的迅猛发展和数字化浪潮的推进&#xff0c;数字乡村作为新时代农村发展的重要战略&#xff0c;正日益成为引领农村现代化的强大引擎。数字乡村不仅代表着农村信息化建设的新高度&#xff0c;更是农村经济社会发展的重要支撑。通过数字技术的深入应用&#xff0c;农…

【C#】读取指定XML节点

&#x1f4f0;XML文件 <?xml version"1.0" encoding"utf-8"?> <configuration><userSettings><Internal.Settings type"Desktop"><setting name"StatsDisplayCount" serializeAs"String">…