当前位置: 首页 > news >正文

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.RedissonLockprivate <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() {@Overridepublic void run(Timeout timeout) throws Exception {// ......// 调用Lua脚本刷新锁的有效时间RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {// loggerreturn;}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.将秒杀优惠券的库存保存到RedisString 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.库存不足,返回1return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,说明是重复下单,返回2return 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) {// 结果为空或者不为0String 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.返回订单IDlog.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 {@Overridepublic 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.VoucherOrderServiceImplprivate class VoucherOrderHandler implements Runnable {@Overridepublic 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为面试赋能(持续更新中…)

相关文章:

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

↑↑↑请在文章开头处下载测试项目源代码↑↑↑ 文章目录 前言4.5 分布式锁-Redisson4.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.…...

什么是多路复用器滤波器

本章将更深入地介绍多路复用器滤波器&#xff0c;以及它们如何用于各种应用中。您将了解到多路复用器如何帮助设计人员创造出更复杂的无线产品。 了解多路复用器 多路复用器是一组射频(RF)滤波器&#xff0c;它们组合在一起&#xff0c;但不会彼此加载&#xff0c;可以在输出之…...

Severt和tomcat的使用(补充)

打包程序 在pom.xml中添加上述代码之后打包时会生成war包并且包的名称是test 默认情况打的是jar包.jar里量但是tomcat要求的是war包. war包Tomcat专属的压缩包. war里面不光有.class还有一些tomcat要求的配置文件(web.xml等)还有前端的一些代码(html, css, js) 点击其右边的m…...

JavaEE初阶——多线程(一)

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享多线程的第一部分:引入线程以及创建多线程的几种方式 此文章是建立在前一篇文章进程的基础上的 如果有不足的或者错误的请您指出! 1.认识线程 我们知道现代的cpu大多都是多核心…...

MongoDB主从复制模式基于银河麒麟V10系统

MongoDB主从复制模式基于银河麒麟V10系统 背景介绍 MongoDB自4.0版本开始已经不再建议使用传统的master/slave复制架构,而是全面采用了复制集(Replica Sets)作为标准的复制和高可用性解决方案。 复制集是MongoDB的一种数据复制和高可用性机制,通过异步同步数据至多个服务…...

Vue使用高德地图

1.在高德平台注册账号 2.我的 > 管理管理中添加Key 3.安装依赖 npm i amap/amap-jsapi-loader --save 或 yarn add amap/amap-jsapi-loader --save 4.导入 AMapLoade import AMapLoader from amap/amap-jsapi-loader; 5.直接上代码&#xff0c;做好了注释&#xff08;初始化…...

2024-04-07(复盘前端)

---HTML 1.HTMl骨架 html&#xff1a;整个网页 head&#xff1a;网页头部&#xff0c;用来存放给浏览器看的信息&#xff0c;如css body&#xff1a;网页主体&#xff0c;用来存放给用户看的信息&#xff0c;例如图片和文字 2.标题标签中h1标签只能使用一次&#xff0c;其…...

SpringCloud学习(10)-SpringCloudAlibaba-Nacos服务注册、配置中心

Spring Cloud Alibaba 参考文档 Spring Cloud Alibaba 参考文档 nacos下载Nacos 快速开始 直接进入bin包 运行cmd命令&#xff1a;startup.cmd -m standalone 运行成功后通过http://localhost:8848/nacos进入nacos可视化页面&#xff0c;账号密码默认都是nacos Nacos服务注…...

OKCC外呼中心配置的电话系统规则

OKCC外呼中心配置电话系统规则可能涉及多个方面&#xff0c;包括呼叫路由、自动化流程、电话接听策略等。以下是一般步骤及注意事项&#xff1a; 呼叫路由配置&#xff1a; 确定呼叫中心的呼叫路由策略&#xff0c;包括如何分配呼叫给不同的坐席或部门。设置呼叫路由规则&#…...

AI推介-大语言模型LLMs论文速览(arXiv方向):2024.03.31-2024.04.05

文章目录~ 1.AutoWebGLM: Bootstrap And Reinforce A Large Language Model-based Web Navigating Agent2.Training LLMs over Neurally Compressed Text3.Unveiling LLMs: The Evolution of Latent Representations in a Temporal Knowledge Graph4.Visualization-of-Thought …...

性能测试工具 ab(Apache Bench)使用详解

Apache Bench (ab) 是一个由 Apache 提供的非常流行的、简单的性能测试工具&#xff0c;用于对 HTTP 服务器进行压力测试。下面是 ab 工具的一些基本使用方法。 安装 在大多数 Unix 系统中&#xff0c;ab 通常作为 Apache HTTP 服务器的一部分预装在系统中。你可以通过在终端…...

智能网联汽车自动驾驶数据记录系统DSSAD数据元素

目录 第一章 数据元素分级 第二章 数据元素分类 第三章 数据元素基本信息表 表1 车辆及自动驾驶数据记录系统基本信息 表2 车辆状态及动态信息 表3 自动驾驶系统运行信息 表4 行车环境信息 表5 驾驶员操作及状态信息 第一章 数据元素分级 自动驾驶数据记录系统记录的数…...

Ubuntu 20.04.06 PCL C++学习记录(十八)

[TOC]PCL中点云分割模块的学习 学习背景 参考书籍&#xff1a;《点云库PCL从入门到精通》以及官方代码PCL官方代码链接,&#xff0c;PCL版本为1.10.0&#xff0c;CMake版本为3.16 学习内容 PCL中实现欧式聚类提取。在点云处理中,聚类是一种常见的任务,它将点云数据划分为多…...

细雨踏春日,新会公安护平安

春雨起&#xff0c;清明至。又是一年春草绿&#xff0c;又是一年清明时。细雨踏春日&#xff0c;思怀故人时&#xff0c;是哀思&#xff0c;亦是相聚。新会公安一抹抹葵乡春日“警”色坚守岗位&#xff0c;确保清明祭扫平稳有序&#xff0c;为人民群众的平安保驾护航。 为确保2…...

3d怎么在一块模型上开个孔---模大狮模型网

在进行3D建模时&#xff0c;有时候需要在模型上创建孔&#xff0c;以实现特定的设计需求或功能。无论是为了添加细节&#xff0c;还是为了实现功能性的要求&#xff0c;创建孔都是常见的操作之一。本文将介绍在3D模型上创建孔的几种常用方法&#xff0c;帮助您轻松实现这一目标…...

Python景区票务人脸识别系统(V2.0),附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…...

全球化业务的网络安全挑战

随着企业业务的全球化&#xff0c;跨国数据传输和用户跨地域访问成为常态。这不仅带来了巨大的商业机会&#xff0c;也带来了以下网络安全挑战&#xff1a; 数据泄露风险&#xff1a;跨国数据传输增加了数据被截获和泄露的风险。访问限制&#xff1a;某些地区可能对互联网内容…...

SQL简单优化思路

在编写SQL查询时&#xff0c;优化查询性能是一个重要的考虑因素&#xff0c;特别是在处理多表连接&#xff08;JOIN&#xff09;和子查询时。以下是一些具体的技巧和最佳实践&#xff0c;可以帮助你在保持相同返回值的前提下&#xff0c;降低SQL执行速度&#xff1a; 明确连接顺…...

外包干了25天,技术倒退明显

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01; 而我已经在一个企业干了四年的功能…...

webpack环境配置分类结合vue使用

文件目录结构 按照目录结构创建好文件 控制台执行: npm install /config/webpack.common.jsconst path require(path) const {merge} require(webpack-merge) const {CleanWebpackPlugin} require(clean-webpack-plugin) const { VueLoaderPlugin } require(vue-loader); c…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

3403. 从盒子中找出字典序最大的字符串 I

3403. 从盒子中找出字典序最大的字符串 I 题目链接&#xff1a;3403. 从盒子中找出字典序最大的字符串 I 代码如下&#xff1a; class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题

在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件&#xff0c;这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下&#xff0c;实现高效测试与快速迭代&#xff1f;这一命题正考验着…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发&#xff0c;实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构&#xff0c;服务器端使用Java Servlet处理请求&#xff0c;数据库采用MySQL存储信息&#xff0…...

AI语音助手的Python实现

引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...

Python竞赛环境搭建全攻略

Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型&#xff08;算法、数据分析、机器学习等&#xff09;不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...

如何配置一个sql server使得其它用户可以通过excel odbc获取数据

要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据&#xff0c;你需要完成以下配置步骤&#xff1a; ✅ 一、在 SQL Server 端配置&#xff08;服务器设置&#xff09; 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到&#xff1a;SQL Server 网络配…...

es6+和css3新增的特性有哪些

一&#xff1a;ECMAScript 新特性&#xff08;ES6&#xff09; ES6 (2015) - 革命性更新 1&#xff0c;记住的方法&#xff0c;从一个方法里面用到了哪些技术 1&#xff0c;let /const块级作用域声明2&#xff0c;**默认参数**&#xff1a;函数参数可以设置默认值。3&#x…...

AWS vs 阿里云:功能、服务与性能对比指南

在云计算领域&#xff0c;Amazon Web Services (AWS) 和阿里云 (Alibaba Cloud) 是全球领先的提供商&#xff0c;各自在功能范围、服务生态系统、性能表现和适用场景上具有独特优势。基于提供的引用[1]-[5]&#xff0c;我将从功能、服务和性能三个方面进行结构化对比分析&#…...