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.…...

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

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

JavaEE初阶——多线程(一)
T04BF 👋专栏: 算法|JAVA|MySQL|C语言 🫵 小比特 大梦想 此篇文章与大家分享多线程的第一部分:引入线程以及创建多线程的几种方式 此文章是建立在前一篇文章进程的基础上的 如果有不足的或者错误的请您指出! 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.直接上代码,做好了注释(初始化…...
2024-04-07(复盘前端)
---HTML 1.HTMl骨架 html:整个网页 head:网页头部,用来存放给浏览器看的信息,如css body:网页主体,用来存放给用户看的信息,例如图片和文字 2.标题标签中h1标签只能使用一次,其…...

SpringCloud学习(10)-SpringCloudAlibaba-Nacos服务注册、配置中心
Spring Cloud Alibaba 参考文档 Spring Cloud Alibaba 参考文档 nacos下载Nacos 快速开始 直接进入bin包 运行cmd命令:startup.cmd -m standalone 运行成功后通过http://localhost:8848/nacos进入nacos可视化页面,账号密码默认都是nacos Nacos服务注…...
OKCC外呼中心配置的电话系统规则
OKCC外呼中心配置电话系统规则可能涉及多个方面,包括呼叫路由、自动化流程、电话接听策略等。以下是一般步骤及注意事项: 呼叫路由配置: 确定呼叫中心的呼叫路由策略,包括如何分配呼叫给不同的坐席或部门。设置呼叫路由规则&#…...
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 提供的非常流行的、简单的性能测试工具,用于对 HTTP 服务器进行压力测试。下面是 ab 工具的一些基本使用方法。 安装 在大多数 Unix 系统中,ab 通常作为 Apache HTTP 服务器的一部分预装在系统中。你可以通过在终端…...

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

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

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

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

Python景区票务人脸识别系统(V2.0),附源码
博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇…...
全球化业务的网络安全挑战
随着企业业务的全球化,跨国数据传输和用户跨地域访问成为常态。这不仅带来了巨大的商业机会,也带来了以下网络安全挑战: 数据泄露风险:跨国数据传输增加了数据被截获和泄露的风险。访问限制:某些地区可能对互联网内容…...
SQL简单优化思路
在编写SQL查询时,优化查询性能是一个重要的考虑因素,特别是在处理多表连接(JOIN)和子查询时。以下是一些具体的技巧和最佳实践,可以帮助你在保持相同返回值的前提下,降低SQL执行速度: 明确连接顺…...

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

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…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解
在 C/C 编程的编译和链接过程中,附加包含目录、附加库目录和附加依赖项是三个至关重要的设置,它们相互配合,确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中,这些概念容易让人混淆,但深入理解它们的作用和联…...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...

手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...