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

Redis企业开发实战(五)——点评项目之分布式锁Redission与秒杀优化

目录

一、Redisson

(一)Redisson基本介绍 

(二)Redisson入门

1.引入依赖

2.配置Redisson客户端

3.使用Redission的分布式锁

4.tryLock参数解析

4.1tryLock()

4.2tryLock(long waitTime, TimeUnit unit)

4.3tryLock(long waitTime, long leaseTime, TimeUnit unit)

4.4注意事项

(三)Redisson可重入锁原理

(四)redission锁重试和WatchDog机制 

(五)redission锁的MutiLock原理

二、秒杀优化 

(一)异步秒杀思路

1.优惠券秒杀下单流程

2.优化方案

3.整体思路

(二)Redis完成秒杀资格判断 

1.保存秒杀优惠券的库存到redis中

2.重启项目,新增优惠券

3.编写seckill.lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

4.从阻塞队列中获取优惠券信息,开启异步线程执行下单操作

5.秒杀业务的优化思路总结


一、Redisson

(一)Redisson基本介绍 

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能:

官网地址: https://redisson.org

GitHub地址: GitHub - redisson/redisson: Redisson - Valkey and Redis Java client. Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..

(二)Redisson入门

1.引入依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>

2.配置Redisson客户端

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {// 配置Config config = new Config();// 创建RedissonClient对象config.useSingleServer().setAddress("redis://192.168.22.145:6379").setPassword("root");// 创建RedissonClient对象return Redisson.create(config);}
}

3.使用Redission的分布式锁

@Resource
private RedissonClient redissonClient;@Override
public Result seckillVoucher(Long voucherId) {// 查询优惠券是否存在SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);if (seckillVoucher == null) {return Result.fail("优惠券不存在");}// 查询秒杀是否开始LocalDateTime beginTime = seckillVoucher.getBeginTime();if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}// 查询秒杀是否结束LocalDateTime endTime = seckillVoucher.getEndTime();if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 判断库存是否充足Integer stock = seckillVoucher.getStock();if (stock < 1) {return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();   // 获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("lock:order:" + userId);// 尝试获取锁boolean isLock = lock.tryLock();// 判断获取锁是否成功if (!isLock) {// 获取锁失败返回错误信息return Result.fail("不允许重复下单!");}// 获取锁成功进行事务操作try {// 获取和事务有关的代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();// 返回订单idreturn proxy.createVoucherOrder(voucherId);} finally {// 最后必须要释放锁// simpleRedisLock.unLock();lock.unlock();}
}

4.tryLock参数解析

tryLock()还有其他几个重载版本,可以接受不同的参数来提供更灵活的锁定行为:

4.1tryLock()
boolean isLock = lock.tryLock();

描述:尝试立即获取锁。
返回值:如果成功获取锁,则返回true;否则返回false。

4.2tryLock(long waitTime, TimeUnit unit)
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);

参数:

  1. waitTime: 等待获取锁的最大时间。
  2. unit: 时间单位(如TimeUnit.SECONDS,TimeUnit.MILLISECONDS等)。

描述:尝试获取锁,直到指定的等待时间结束。如果在这段时间内成功获取了锁,则返回true;如果超时仍未获取到锁,则返回false。

异常:可能会抛出InterruptedException如果当前线程在等待过程中被中断。

4.3tryLock(long waitTime, long leaseTime, TimeUnit unit)
boolean isLock = lock.tryLock(10, 15, TimeUnit.SECONDS);

参数:

  1. waitTime: 等待获取锁的最大时间。
  2. leaseTime: 锁的租约时间,在此时间后锁将自动释放(即使持有锁的客户端未显式解锁)。
  3. unit: 时间单位(如TimeUnit.SECONDS,TimeUnit.MILLISECONDS等)。

描述:尝试获取锁,最多等待waitTime指定的时间。一旦获取锁,将在leaseTime后自动释放锁,除非在此之前已经显式调用了unlock()方法。
异常:可能会抛出InterruptedException如果当前线程在等待过程中被中断。

示例代码

最多等待 10 秒钟来获取锁,同时设置锁的租约时间为 15 秒:

RLock lock = redissonClient.getLock("lock:order:" + userId);
try {boolean isLockAcquired = lock.tryLock(10, 15, TimeUnit.SECONDS);if (isLockAcquired) {// 成功获取锁,执行业务逻辑// ...} else {// 未能获取锁,处理失败情况// ...}
} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态// 处理中断异常
} finally {if (lock.isHeldByCurrentThread()) {lock.unlock(); // 确保锁被正确释放}
}
4.4注意事项
  • 中断处理:当调用tryLock(...)方法时,可能会抛出InterruptedException,可以通过调用Thread.currentThread().interrupt(),并根据需要进行进一步的错误处理。
  • 锁的释放:最好是在finally块中调用unlock(),以保证即使发生异常也能正确释放锁。
  • 租约时间:如果任务执行时间较长,应确保租约时间足够长以避免锁提前被释放。然而,过长的租约时间也可能导致死锁问题,因此需要权衡考虑。

(三)Redisson可重入锁原理

  • 可重入锁允许同一个线程多次获取同一把锁而不会发生死锁。这意味着如果一个线程已经持有了某个锁,并试图再次获取该锁,它将成功获取而不会被阻塞。这为递归调用或在一个方法内多次需要获取同一资源的场景提供了便利。
  • 不可重入锁不允许同一个线程多次获取同一把锁。如果一个线程已经持有了某一把锁,并试图再次获取该锁,则会被阻塞,导致程序陷入死锁状态。

 

tryLock源码底层调用的获取锁的脚本

unlock源码底层调用的释放锁的脚本

(四)redission锁重试和WatchDog机制 

可重入 :利用 hash 结构记录线程 id 和重入次数
可重试 :利用信号量和 PubSub 功能实现等待、唤醒,获取锁失败的重试机制
超时续约 :利用 watchDog,watchDog默认是30秒,每隔一段时间10s(releaseTime / 3 ),重置超时时间

(五)redission锁的MutiLock原理

        为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

        此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

        为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。 

MutiLock加锁原理:

        当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试。

二、秒杀优化 

(一)异步秒杀思路

1.优惠券秒杀下单流程

        当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤:

2.优化方案

        我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点

        第一个难点是我们怎么在redis中去快速校验一人一单,还有库存判断

        第二个难点是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。 

3.整体思路

        当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束;如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠券存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua脚本来操作。

        当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。

(二)Redis完成秒杀资格判断 

业务实现步骤:

新增秒杀优惠券的同时,将优惠券信息保存到Redis

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

如果抢购成功,将优惠券id和用户id封装后存入阻塞队列

开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

1.保存秒杀优惠券的库存到redis中

VoucherServiceImpl

@Resource
private StringRedisTemplate stringRedisTemplate;@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucher seckillVoucher = new SeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀到redis中,库存信息不需要考虑有效期stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

2.重启项目,新增优惠券

可以看到数据库和redis中都新增的优惠券数据:

3.编写seckill.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

重构seckillVoucher方法: 

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}@Overridepublic Result seckillVoucher(Long voucherId) {// 1.执行lua脚本Long userId = UserHolder.getUser().getId();Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString());// 2.判断结果是否为0int r = result.intValue();if (r != 0) {// 2.1 不为0,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 2.2 为0,代表有购买资格,把下单信息保存到阻塞队列中long orderId = redisIdWorker.nextId("order");// TODO 保存阻塞队列// 3.返回订单idreturn Result.ok(orderId);}
}

重启项目后,进行优惠券下单测试: 

优惠券库存成功减少:

已经下过订单的用户id也被存储到set集合中:

再次请求会提示重复下单:

4.从阻塞队列中获取优惠券信息,开启异步线程执行下单操作

先恢复redis中优惠券库存数据:

代码:

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}// 创建阻塞队列private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);// 实现异步下单的线程池,给一个即可,速度不需要太快private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();// 需要让VoucherOrderServiceImpl类初始化的时候就执行线程池中的任务@PostConstructprivate void init() {// 类初始完成后,提交线程池中的任务SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}// 线程池执行的任务private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {// 1.获取队列中的订单信息try {// 从队列中获取并移除队列头部的元素。// 如果队列为空,take() 方法会阻塞当前线程,直到队列中有可用的元素。VoucherOrder voucherOrder = orderTasks.take();// 2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}// 子线程中执行创建订单的逻辑private void handleVoucherOrder(VoucherOrder voucherOrder) {// 获取用户Long userId = voucherOrder.getUserId();// 获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("lock:order:" + userId);// 尝试获取锁boolean isLock = lock.tryLock();// 判断获取锁是否成功if (!isLock) {// 获取锁失败log.error("不允许重复下单");return;}try {// 这里无法通过代理对象调用事务,因为该业务逻辑是在子线程中执行的// 因此,我们需要提前加载事务对象proxy.createVoucherOrder(voucherOrder);} finally {// 释放锁lock.unlock();}}// 初始化代理对象private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.执行lua脚本Long userId = UserHolder.getUser().getId();Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString());// 2.判断结果是否为0int r = result.intValue();if (r != 0) {// 2.1 不为0,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单");}// 2.2 为0,代表有购买资格,把下单信息保存到阻塞队列中// 2.3创建订单VoucherOrder voucherOrder = new VoucherOrder();// 2.4订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 2.5用户idvoucherOrder.setUserId(userId);// 2.6代金券idvoucherOrder.setVoucherId(voucherId);// 2.7将订单信息放入阻塞队列中orderTasks.add(voucherOrder);// 开启异步下单,需要准备线程池// 3.在主线程中获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy();// 3.返回订单idreturn Result.ok(orderId);}// 异步请求时创建的订单逻辑@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {// 获取用户idLong userId = voucherOrder.getUserId();// 查询该用户的订单int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 判断订单是否存在if (count > 0) {// 表示用户已经购买过了log.error("用户已经购买过一次!");return;}// 如果订单不存在,则扣减优惠券库存// UPDATE seckill_voucher SET stock = stock - 1 WHERE voucher_id = ? AND stock > 0;boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();// 如果扣减优惠券库存失败,则返回错误信息if (!success){log.error("库存不足");return;}// 创建订单save(voucherOrder);}
}

第一次测试下单: 

第二次测试下单:

数据库优惠券被正常删减,订单被正常添加:

redis中的数据也被成功生成:

5.秒杀业务的优化思路总结

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务

  • 再将下单业务放入阻塞队列,利用独立线程异步下单

  • 基于阻塞队列的异步秒杀存在的问题:内存限制问题;数据安全问题(系统出现故障时,队列中的任务被移除,导致数据丢失)

  • 解决阻塞队列的异步秒杀存在问题的办法:使用消息队列

相关文章:

Redis企业开发实战(五)——点评项目之分布式锁Redission与秒杀优化

目录 一、Redisson (一)Redisson基本介绍 (二)Redisson入门 1.引入依赖 2.配置Redisson客户端 3.使用Redission的分布式锁 4.tryLock参数解析 4.1tryLock() 4.2tryLock(long waitTime, TimeUnit unit) 4.3tryLock(long waitTime, long leaseTime, TimeUnit unit) 4…...

IDEA安装离线插件(目前提供了MavenHelper安装包)

目录 1、离线安装方式2、Maven Helper 1、离线安装方式 首先访问 IDEA插件网站 下载离线插件安装包&#xff0c;操作如下&#xff1a; 然后打开IDEA的Settings配置&#xff0c;点击Plugins&#xff0c;点击右侧设置按钮&#xff08;齿轮&#xff09;&#xff0c;选择Install P…...

LabVIEW 开发航天项目软件

在航天项目软件开发中&#xff0c;LabVIEW 凭借其图形化编程优势被广泛应用。然而&#xff0c;航天项目的高可靠性、高精度及复杂环境适应性要求&#xff0c;使得在使用 LabVIEW 开发时&#xff0c;有诸多关键要点需要特别关注。本文将详细分析在开发航天项目软件时需要重点注意…...

互联网大厂中面试的高频计算机网络问题及详解

前言 哈喽各位小伙伴们,本期小梁给大家带来了互联网大厂中计算机网络部分的高频面试题,本文会以通俗易懂的语言以及图解形式描述,希望能给大家的面试带来一点帮助,祝大家offer拿到手软!!! 话不多说,我们立刻进入本期正题! 一、计算机网络基础部分 1 先来说说计算机网…...

如何定义“破坏环境”

当我们谈论破坏环境时&#xff0c;通常会从人类活动对自然生态造成负面影响的角度来定义。例如&#xff0c;大规模的森林砍伐、工业污染排放、温室气体增加等&#xff0c;都是典型的破坏环境的行为。我们常常看到这些行为导致了生态系统的破坏、物种灭绝、气候变化等问题&#…...

WPS接入DeepSeek模型

1.wps 下载安装 WPS-支持多人在线协作编辑Word、Excel和PPT文档_WPS官方网站 &#xff08;最好是安装最新的wps&#xff09; 2.offieceAi工具下载安装 软件下载 | OfficeAI助手 下载后安装下载下来的两个工具。安装路径可以自行修改 3.打开WPS,点击文件-》 选项-》信任中心 勾…...

自然语言处理NLP_[1]-NLP入门

文章目录 1.自然语言处理入门1. 什么是自然语言处理2.自然语言处理的发展简史3 自然语言处理的应用场景1. **机器翻译**2. **文本分类**3. **情感分析**4. **问答系统**5. **文本生成**6. **信息抽取**7. **语音识别与合成**8. **文本摘要**9. **搜索引擎优化**10. **聊天机器人…...

详解在Pytest中忽略测试目录的三种方法

关注开源优测不迷路 大数据测试过程、策略及挑战 测试框架原理&#xff0c;构建成功的基石 在自动化测试工作之前&#xff0c;你应该知道的10条建议 在自动化测试中&#xff0c;重要的不是工具 你是否曾因无关或过时的代码导致测试失败&#xff1f; 这可能会增加调试和故障排除…...

IDEA中列举的是否是SpringBoot的依赖项的全部?在哪里能查到所有依赖项,如何开发自己的依赖项让别人使用

在 IntelliJ IDEA 中列举的依赖项并不一定是 Spring Boot 项目的全部依赖项。IDEA 通常只显示你在 pom.xml&#xff08;Maven&#xff09;或 build.gradle&#xff08;Gradle&#xff09;中显式声明的依赖项&#xff0c;而这些依赖项本身可能还会引入其他传递性依赖。 1. 如何…...

ECG分析0210

指标计算方法 1. HR (心率&#xff0c;Heart Rate)&#xff1a; 心率是每分钟心跳的次数。它通常通过计算RR间期&#xff08;即两次R波之间的时间间隔&#xff09;来获得。 计算方法&#xff1a; 首先&#xff0c;检测到R波的位置&#xff08;例如通过find_peaks函数检测&a…...

计算机毕业设计Python+Spark知识图谱医生推荐系统 医生门诊预测系统 医生数据分析 医生可视化 医疗数据分析 医生爬虫 大数据毕业设计 机器学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

JavaScript:CPU缓存预取以及确定数据下直接更改数组length的好处

CPU缓存预取以及确定数据下直接更改数组length的好处 1. CPU 缓存预取&#xff08;Cache Preloading&#xff09;&#xff1a;CPU 缓存预取&#xff1a;为什么反向填充栈能利用缓存预取&#xff1a; 2. 为什么可以直接改变数组的 length&#xff1a;数组的动态长度&#xff1a;…...

Selenium常用自动化函数

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:数据库 JavaEE专栏:JavaEE 软件测试专栏:软件测试 关注博主带你了解更多知识 目录 1.元素的定位 1.1 定位步骤 1,要想定位,就先打开开发者工具 2,先点击左上角图标 1.2 cssSelector 1.3 xpath 2.操作测…...

【故障排除】ls: command not found 终端命令失效的解决办法

【TroubleShooting】ls: command not found 终端命令失效的解决办法 A Solution to Solve “Command not found” of Terminal on Mac 一直在使用心爱的MacBook Pro的Terminal&#xff0c;并且为她定制了不同的Profile。 这样&#xff0c;看起来她可以在不同季节&#xff0c…...

OpenStack-Train版-Allinone自动化部署脚本

一、环境准备 操作系统&#xff1a;CentOS 7 或以上版本 建议配置&#xff1a; CPU&#xff1a;8 核或以上 内存&#xff1a;16 GB 或以上 磁盘&#xff1a;500 GB 或以上 网络配置&#xff1a; 确保虚拟机已配置静态 IP 地址 确保虚拟机可以正常访问外部网络 二、自动…...

12.翻转、对称二叉树,二叉树的深度

反转二叉树 递归写法 很简单 class Solution { public:TreeNode* invertTree(TreeNode* root) {if(rootnullptr)return root;TreeNode* tmp;tmproot->left;root->leftroot->right;root->righttmp;invertTree(root->left);invertTree(root->right);return …...

新电脑配置安装下载

1、谷歌浏览器 地址https://www.google.cn/chrome/ 下载安装即可。 2、nvm下载 下载地址&#xff1a;地址https://nvm.uihtm.com/#google_vignette nvm install 相对应的node版本 // 安装 nvm list 可以查看已下载的node版本 // 查看 nvm use 相对应的node版本号 // 使用 nv…...

数字孪生智慧停车管理可视化平台

采用图扑可视化技术搭建智慧停车管理平台&#xff0c;实现了全面的数据整合与实时监控&#xff0c;提升了停车场运营效率和用户体验。通过 HT 可视化界面&#xff0c;管理者能够实时观察和分析停车位使用情况&#xff0c;进行精准调度与优化决策。...

win10 llamafactory模型微调相关②

微调 使用微调神器LLaMA-Factory轻松改变大语言模型的自我认知_llamafactory 自我认知-CSDN博客 【大模型微调】使用Llama Factory实现中文llama3微调_哔哩哔哩_bilibili 样本数据集 &#xff08;数据集管理脚本处需更改&#xff0c;见报错解决参考1&#xff09; 自我认知微…...

车载测试工具 --- CANoe VH6501 进行Not Acknowledge (NAck) 测试

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…...

Mysql中存储引擎各种介绍以及应用场景、优缺点

概述 MySQL 提供了多种存储引擎&#xff0c;每种引擎有不同的特点和适用场景。以下是几种常见的 MySQL 存储引擎的详细介绍&#xff0c;包括它们的底层工作原理、优缺点&#xff0c;以及为什么 MySQL 默认选择某种引擎。 1. InnoDB 底层工作原理&#xff1a; 事务支持&#…...

使用 AlexNet 实现图片分类 | PyTorch 深度学习实战

前一篇文章&#xff0c;CNN 卷积神经网络处理图片任务 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 本篇文章内容来自于 强化学习必修课&#xff1a;引领人工智能新时代【梗直哥瞿炜】 使用 AlexNet 实现图片分类…...

Linux系统引导与服务管理

目录 一、Linux引导过程 1、引导过程概述 1.1、BIOS开机自检 1.2、MBR读取 1.3、加载引导加载程序&#xff08;GRUB&#xff09; 1.4、内核加载 1.5、初始化进程&#xff08;init&#xff09; 二、服务 2.1、服务类型 2.2、服务管理工具 三、运行级别 四、systemd …...

【Hadoop】大数据权限管理工具Ranger2.1.0编译

目录 ​编辑一、下载 ranger源码并编译 二、报错信息 报错1 报错2 报错3 报错4 一、下载 ranger源码并编译 ranger官网 https://ranger.apache.org/download.html 由于Ranger不提供二进制安装包&#xff0c;故需要maven编译。安装其它依赖&#xff1a; yum install gcc …...

宝珀(Blancpain):传承近三百年的机械制表传奇(中英双语)

宝珀&#xff08;Blancpain&#xff09;&#xff1a;传承近三百年的机械制表传奇 在钟表行业中&#xff0c;宝珀&#xff08;Blancpain&#xff09; 作为世界上最古老的制表品牌&#xff0c;一直以其卓越的机械工艺、复杂功能腕表和对创新的坚持而闻名。自 1735 年成立以来&am…...

【Linux】Linux命令:crontab

目录 1、作用2、命令使用格式3、常用参数说明4、时程表4.1 格式4.2 常见问题处理 5、示例 1、作用 crontab命令用于对用户的时程表进行查看、删除、修改等操作。 用户的时程表是用于记录着要定期执行的程序。当安装完Linux操作系统启动后&#xff0c; cron服务会定期执行时程表…...

C++ 使用CURL开源库实现Http/Https的get/post请求进行字串和文件传输

CURL开源库介绍 CURL 是一个功能强大的开源库&#xff0c;用于在各种平台上进行网络数据传输。它支持众多的网络协议&#xff0c;像 HTTP、HTTPS、FTP、SMTP 等&#xff0c;能让开发者方便地在程序里实现与远程服务器的通信。 CURL 可以在 Windows、Linux、macOS 等多种操作系…...

浙江大华社招面试

下面是我之前社招面试大华时,面得是嵌入式Linux系统工程师,下面是我初试所被问到的问题分享给大家 毕业之后工作负责过哪些产品,工作负责哪些内容 Camera相关 1、调试sensor是多少像素 2、板子上怎么连接sensor 3、几LINE 4、每个LINE的data rate 是多少 ,单位是什么 5、图…...

多对多的增删改查

一 : 增 随机单号: /*** 文档就绪函数*/$(function () {//随机单号let number Math.floor(Math.random()*(9999-10001)1000);//取随机单号的值 固定格式输出$("#docNo").val(BSnumber);}) 开单日期: //处理开单日期$("#invoiceDate").val(new Date().to…...

vscode设置保存时自动缩进和格式化

参考博客 如何在 VSCode 中自动缩进你的代码 | Linux 中国 省流 使用 Ctrl Shift P 来打开命令模式&#xff0c;搜索 Open User Settings 并按下回车你需要搜索 Auto Indent&#xff0c;并在 “编辑器&#xff1a;自动缩进(Editor: Auto Indent)” 中选择 “全部(Full)”P…...