当前位置: 首页 > 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…...

【蓝桥杯嵌入式】第十三届省赛(第二场)

目录 0 前言 1 展示 1.1 源码 1.2 演示视频 1.3 题目展示 2 CubeMX配置(第十三届省赛第二场真题) 2.1 设置下载线 2.2 HSE时钟设置 2.3 时钟树配置 2.4 生成代码设置 2.5 USART1 2.5.1 基本配置 2.5.2 NVIC 2.5.3 DMA 2.6 TIM 2.6.1 TIM2 2.6.2 TIM4 2.6.3 …...

maya节点绕轴旋转

目录 旋转后并尝试冻结变换 绕x轴旋转90度 使用Python脚本 使用图形界面 使用MEL脚本 绕y轴旋转90度 使用Python脚本 ok 旋转后并尝试冻结变换 import maya.cmds as cmdsdef adjust_root_rotation_for_export(joint_name):# 选择根节点cmds.select(joint_name)# 应用旋…...

如何水出第一篇SCI:SCI发刊历程,从0到1全过程经验分享!!!

如何水出第一篇SCI&#xff1a;SCI发刊历程&#xff0c;从0到1全路程经验分享&#xff01;&#xff01;&#xff01; 详细的改进教程以及源码&#xff0c;戳这&#xff01;戳这&#xff01;&#xff01;戳这&#xff01;&#xff01;&#xff01;B站&#xff1a;Ai学术叫叫兽e…...

SpringBoot表单防止重复提交

哪些因素会引起重复提交&#xff1f; 开发的项目中可能会出现下面这些情况&#xff1a; 前端下单按钮重复点击导致订单创建多次 网速等原因造成页面卡顿&#xff0c;用户重复刷新提交请求 黑客或恶意用户使用postman等http工具重复恶意提交表单 重复提交会带来哪些问题&…...

java面向对象.day17(什么是面向对象)

先认识&#xff1a;面向过程思想&#xff0c;面向对象思想 面向过程思想&#xff08;具体&#xff09; 步骤清晰简单&#xff0c;第一步做什么&#xff0c;第二步做什么.... 面对过程适合处理一些较为简单的问题 面向对象思想&#xff08;抽象&#xff09; 物以类聚&#x…...

mysql处理并发简单示例

处理并发的基本思路是使用锁来控制对共享资源的访问。在MySQL中&#xff0c;可以使用事务和行级锁来处理并发。 具体处理方式如下&#xff1a; 创建一个用于存储并发任务的MySQL表&#xff0c;该表包含一个自增的ID字段和任务名称字段。设置一个最大并发数量&#xff0c;用来…...

顺序表——功能实现

✨✨欢迎&#x1f44d;&#x1f44d;点赞☕️☕️收藏✍✍评论 个人主页&#xff1a;秋邱博客 所属栏目&#xff1a;C语言 &#xff08;感谢您的光临&#xff0c;您的光临蓬荜生辉&#xff09; 目录 1.0 前言 2.0 线性表 2.1 顺序表 2.2 顺序表的分类 2.3 顺序表功能的实现…...

达梦导出工具dexp

基础环境 操作系统&#xff1a;Red Hat Enterprise Linux Server release 7.9 (Maipo) 数据库版本&#xff1a;DM Database Server 64 V8 架构&#xff1a;单实例dexp 逻辑导出 dexp 工具可以对本地或者远程数据库进行数据库级、用户级、模式级和表级的逻辑备份。备份的内容非…...

Ubuntu 22.04安装新硬盘并启动时自动挂载

方法一 要在Ubuntu 22.04系统中安装一个新硬盘、对其进行格式化并实现启动时自动挂载&#xff0c;需要按以下步骤操作&#xff1a; 1. 安装硬盘 - 确保你的硬盘正确连接到计算机上&#xff08;涉及硬件安装&#xff09;。 2. 发现新硬盘 - 在系统启动后&#xff0c;打开终端…...

Mybatis中sqlSession.getMapper背后的原理

在通过MyBatis操作数据库之前我们一定先通过Session对象获取指定Mappper接口的代理对象。如下代码所示&#xff1a; public class UserMapper{Select(value"SELECT * FROM user")public List<User> findAll(); }public static void main(String [] args){Conf…...

做网站自己申请域名还是建站公司/微信小程序开发费用一览表

NOI /2.4基本算法之分治2991:2011 2011链接 此题乍一看&#xff0c;嚯哟&#xff0c;直接上手&#xff0c;用个循环不断%10000就搞定&#xff0c;结果来个TLE把俺给整蒙了&#xff0c;结果一细看&#xff0c;嗨呀&#xff0c;不是K小于等于200&#xff0c;是它的位数。如此多…...

青岛有没有做网站的/网站关键词优化推广哪家快

对于女人来说&#xff0c;口红是不可缺少的时尚单品&#xff0c;她们能准确的说出每一支口红的色号&#xff0c;这相比于直男看口红色号是一种&#xff0c;就显得女人特别精致。女人的口红是用不完的&#xff0c;现在呢&#xff0c;我就大家说一下2019最流行的这5支口红&#x…...

济南品牌网站建设介绍/四川seo技术培训

上一节讲了用xml文件的方式配置Bean&#xff0c;虽然可以满足所有要求&#xff0c;但是简单的两个类就配置了那么多内容。后期维护起来很不方便。这一节学习通过注解的方式实现Bean的配置。这里先了解下各个注解代表的含义 controller 控制器&#xff08;注入服务&#xff09; …...

健康网站模版/净水器十大品牌

废话少说&#xff0c;先上程序运行时的图片 开源地址&#xff1a; https://github.com/xland/TuDao 编译好的版本下载地址&#xff1a; https://github.com/xland/TuDao/blob/master/%E5%8F%91%E5%B8%83%E5%8C%85/%E5%B1%A0%E5%88%80.zip?rawtrue 说明&#xff1a; 天猫店整店…...

网站制作公司嘉兴/跨境电商有哪些平台

转载 原帖地址:http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn792608 ------------------------------------------------------------------------------------------------------------------- 许多嵌入式应用领域&#xff0c;软件都是基于输入响应的组织方式&#xff0c…...

延边app网站开发/全网营销推广方案

舞弊三角理论&#xff0c;企业舞弊的产生是由压力、机会和自我合理化&#xff08;借口&#xff09;三要素组成。舞弊要发生&#xff0c;三要素缺一不可。...