分布式锁+秒杀异步优化
文章目录
- 问题
- 思路
- setnx实现
- 锁误删问题和解决方案
- Redis Lua脚本
- 问题引出
- 解决方案
- setnx实现的问题
- Redission
- 快速入门
- redission可重入锁原理
- 秒杀优化(异步优化)
- 异步秒杀思路
- 秒杀资格判断
- Redis消息队列
问题
比如我们两个机器都部署了我们项目,这里nginx使用轮询的方式去做负载均衡
这里用postman测试
出问题了:
两个请求都会进入到synchronized,这两个请求是同一个用户同一个商品,按理来说是一人一单不能同时进入synchronized代码块
但是我们采用了分布式,我们请求1进入的8081端口,请求2进入的8082端口
相同的代码,部署在不同的机器上,synchronized锁不住其他机器/端口上的代码
这样的话还是实现不了一人一单
原理图
一个JVM一个锁监视器,只能保证请求该JVM的线程互斥,保证不了其他JVM的,所以分布式情况下也可能两个线程(userid相同)进入同步代码块,导致线程安全问题
思路
在JVM外单起一个服务,所有节点都会找这个服务去获取锁,这样的话就可以实现不同JVM同一代码块的锁
下面是一些实现方案
方案对应上文提到的服务
下面我们主要讲redis的解决方案
setnx实现
set lock thread1 NX EX 10
NX代表set命令是setnx(不存在可以添加返回1,存在可以失败返回0)
EX(expire)后面代表过期时间为了防止
setnx lock thread1
expire lock 10
两个命令之间redis宕机导致锁设置了但是没有过期时间
我们使用set lock thread1 NX EX 10来同时设置key和过期时间
初版Java代码
锁对象以及一些方法
public interface ILock { /** * 尝试获取锁 * @param timeoutSec 锁持有的超时时间,过期后自动释放 * @return true代表获取锁成功;false代表获取锁失败 */ boolean tryLock(long timeoutSec); /** * 释放锁 */ void unlock();
} public class SimpleRedisLock implements ILock {private String name;private RedisTemplate redisTemplate;public SimpleRedisLock(String name, RedisTemplate redisTemplate) {this.name = name;this.redisTemplate = redisTemplate;}private static final String KEY_PREFIX = "lock:";@Override public boolean tryLock(long timeoutSec) {Long ThreadId = BaseContext.getCurrent().getId();Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+ name, ThreadId+"", timeoutSec, TimeUnit.SECONDS);
// 如果 flag 为 true,返回 true;如果 flag 为 false 或 null,返回 false。return BooleanUtil.isTrue(flag);}@Override public void unlock() {redisTemplate.delete(KEY_PREFIX+ name);}
}
一人一单测试代码
分布式锁(悲观锁)解决一人一单的问题,查后insert语句,乐观锁解决查后update语句
因为insert原来没有数据所以不可以用乐观锁,而update可以
Long userId = UserHolder.getUser().getId();//确保锁的针对同一个用户SimpleRedisLock lock = new SimpleRedisLock("order:"+userId, redisTemplate);boolean isLock = lock.tryLock(1200); // 获取锁if (!isLock) {// 获取锁失败, 返回或者重试return Result.fail("不允许重复下单");//新增这块用的同步锁(分布式),而查询减票是乐观锁}try{// 获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象,其实就是IVoucherOrderService这个接口的代理对象,返回的是Object,做个强转return proxy.createVoucherOrder(voucherId); // 如果报错了是因为我们的接口中没有这个方法,那我们就在接口中创建一下这个方法就行}finally {lock.unlock();// 释放锁}
锁误删问题和解决方案
初始线程阻塞可能释放其他线程的锁
解决方法:释放锁的时候看是不是自己获取的锁
释放锁时观察是否是自己获取的锁
解决释放的锁不是自身原先获取的锁
- 在获取锁时存入线程标识(可以用UUID+ThreadId表示)
- 在释放锁时先获取锁中的线程序示,判断是否与当前线程标识一致
1.如果一致则释放锁
2.如果不一致则不释放锁
why use UUID?
ThreadId的规律是每个JVM创建线程就会自增赋值,这样的话可能JVM1和JVM2相同的线程id同时进入代码,单用ThreadId可能会导致误删的情况
所以这里使用UUID作为表示,由于这里UUID是static属性,所有对象公用一个,只能表示JVM的唯一性,而不能标识JVM中的线程,所以释放锁的时候比对ThreadId
static UUID标识JVM,ThreadID标识JVM里面的线程,保证线程唯一性
private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId+"", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {// 获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁中的标识String id = (String) redisTemplate.opsForValue().get(KEY_PREFIX + name);// 判断标识是否一致if (threadId.equals(id)) {// 释放锁redisTemplate.delete(KEY_PREFIX + name);}}
Redis Lua脚本
问题引出
由于我们判断和释放锁不是原子性操作
如果我们判断后发生了阻塞(JVM垃圾回收),锁超时释放,然后另一个线程获取
之后线程1恢复,因为已经判断,所以还是会删除锁,导致并发问题
解决方案
保证我们判断锁是否为自己的和释放锁的操作为原子的即可
Lua脚本类似于mysql中的事务保证了多个操作的原子性,redis中的事务不同于mysql事务达不到这种效果,所以采用Lua脚本来实现
为什么使用 Lua 脚本?
-
原子性:Lua 脚本在 Redis 中执行时是原子的,即脚本在执行期间不会被其他命令中断。
Lua 脚本在执行期间,Redis 不会处理其他命令,确保脚本的原子性。但长时间运行的脚本可能导致 Redis 阻塞,需谨慎使用。
-
减少网络开销:将多个操作合并为一个脚本,减少客户端与服务器之间的通信次数。
-
复杂操作:Lua 脚本支持条件判断、循环等复杂逻辑,适合处理需要多个步骤的操作。
-
Redis 会缓存加载的脚本,通过 SCRIPT LOAD 加载的脚本会一直保留,直到服务器重启或使用 SCRIPT FLUSH 清除。
- 使用脚本我们需要先编写一个脚本
-
在 Lua 脚本中,可以通过 redis.call() 或 redis.pcall() 调用 Redis 命令。
-
redis.call():执行命令,出错时抛出异常。
-
redis.pcall():执行命令,出错时返回错误信息。
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
KEYS[1] 是键参数,ARGV[1] 是值参数。
- 1 表示有 1 个键参数。
用法
- EVAL:执行 Lua 脚本
EVAL "return 'Hello, Redis!'" 0
其中,“return ‘Hello, Redis!’” 是 Lua 脚本,0 表示没有键参数。
- SCRIPT LOAD:加载脚本到 Redis,返回 SHA1 校验和。
SCRIPT LOAD "return 'Hello, Redis!'"
返回的 SHA1 值可用于后续的 EVALSHA 命令。
- EVALSHA:通过 SHA1 值执行已加载的脚本。(此时已经缓存了)
EVALSHA <SHA1> 0
Lua脚本实现释放分布式锁
-- 这里的 KEYS[1] 就是传入的key,这里的 ARGV[1] 就是当前传递的参数值
-- 获取当前的值,判断是否与当前传递的参数一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then -- 一致,则删除 return redis.call('DEL', KEYS[1])
end
-- 不一致,则返回0
return 0
setnx实现的问题
setnx实现分布式锁问题
不可重入:方法A调用方法B,方法A和方法B都需要获取到该锁资源,若是不可冲入锁,会导致死锁,A执行不完不能释放锁,B拿不到锁导致A执行不完
不可重试:直接返回了false,应为可充实
超时释放:最好可以动态的超时释放
主从一致性:从节点变为主节点时候没有同步到锁的信息,然后其他线程就可以进入了
Redission
Redisson 是一个用于 Java 的 Redis 客户端,它不仅提供了对 Redis 数据库的简单 API 接口,还提供了许多高级功能,旨在简化分布式应用程序的开发。它又以下的特性:
-
丰富的数据结构:提供了多种高级数据结构,如映射、集合、列表等,兼容 Java 集合框架。
-
分布式执行:支持分布式任务处理,实现高并发的任务执行。
-
分布式锁:确保在分布式环境下对共享资源的安全访问。
-
对象映射:自动序列化和反序列化 Java 对象,简化数据存取。
-
反应式编程支持:适合高并发和低延迟的应用程序。
-
高可用性:支持 Redis Sentinel 和 Redis Cluster,确保稳定运行。
快速入门
导入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version>
</dependency>
配置redisson的配置类
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379") //redis://192.168.150.101:6379.setPassword("123456");// 创建RedissonClient对象return Redisson.create(config);}
}
使用Redisson
@Resource
private RedissonClient redissonClient; @Test
void testRedisson() throws InterruptedException { // 获取锁(可重入),指定锁的名称 RLock lock = redissonClient.getLock("anyLock"); // 尝试获取锁,参数分别是:获取锁的最大等待时间(单位是时间尝试),锁自动释放的时间,时间单位 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 无参的话就是不等待,30秒自动释放 // 判断释放锁获取成功 if (isLock) { try { System.out.println("执行业务"); } finally { // 释放锁 lock.unlock(); } }
}
redission可重入锁原理
未完待续
秒杀优化(异步优化)
异步秒杀思路
先回顾一下
当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤
1、查询优惠卷2、判断秒杀库存是否足够3、查询订单4、校验是否是一人一单5、扣减库存6、创建订单
在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢,所以我们需要异步程序执行,那么如何加速呢?
优化方案:
我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,是否一人一单,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功
再在后台开一个线程,后台线程慢慢的去执行queue里边的消息
-
第一个难点是我们怎么在redis中去快速校验一人一单,还有库存判断
-
第二个难点是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。
秒杀资格判断
redis中去做秒杀资格的判断
需要有两个数据1.库存2.用户是否购买过该优惠券
信息1库存可以直接存库存信息
信息2可以是value为set存储购买过该优惠券的用户id进行比对判断
整体思路:当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作
当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。
优化代码
- 首先,在添加优惠券的同时,我们需要将该优惠券及其库存保存到redis中,方便我们之后在redis中快速判断优惠券库存是否充足。对添加优惠券方法做修改如下。
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {// MP保存优惠券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(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
- redis判断采用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)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
redis如何操作执行lua脚本?
1.写好lua脚本在同一目录
2.在需要执行的类中定义并读取lua脚本
3.在执行处使用redistemplate.execute执行并传参,参数是lua里面定义的
//提前初始化脚本,避免每次去执行脚本时单独去创建脚本对象private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//加载lua脚本SECKILL_SCRIPT.setResultType(Long.class);//设置返回值}/*** 使用lua脚本完成扣减资格判断* @param voucherId* @return*/@Override
public Result seckillVoucher(Long voucherId) {//获取用户Long userId = UserHolder.getUser().getId();//生成订单idlong orderId = redisIdWorker.nextId("order");// 1.执行lua脚本 判断是否满足扣减资格(看缓存里面是否有 重复订单)Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),//lua脚本里面没有代表key的参数 这里传入空参voucherId.toString(), userId.toString(), String.valueOf(orderId) //根据lua脚本传入多个参数);int r = result.intValue();//将long类型转换为int 再去判断// 2.判断结果是否为0if (r != 0) {// 2.1.不为0 ,代表没有购买资格return Result.fail(r == 1 ? "库存不足" : "不能重复下单"); //r == 2 代表不能重复下单}//TODO 保存阻塞队列 待完成// 3.返回订单idreturn Result.ok(orderId);
}
Redis消息队列
抢购成功,将优惠券id和用户id封装后存入阻塞队列,然后开启线程池去阻塞队列里拿东西执行
BlockingQueue:阻塞队列对象,当一个线程从阻塞队列get值的时候,如果没有元素就会阻塞
1、创建阻塞队列 ,将 voucherOrder 对象放到队列当中
/*** 初始化阻塞队列*/private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);// 保存阻塞队列 VoucherOrder voucherOrder = new VoucherOrder();// 2.3.订单idvoucherOrder.setId(orderId);// 2.4.用户idvoucherOrder.setUserId(userId);// 2.5.代金券idvoucherOrder.setVoucherId(voucherId);// 2.6.放入阻塞队列orderTasks.add(voucherOrder);
2.异步下单
视频
@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);}@PostConstruct //注解含义:在当前类初始化完毕后执行private void init(){SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}//创建阻塞队列private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);//创建线程池(单线程线程池)private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//内部类,规定线程执行逻辑private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){try {// 1.获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();// 2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常",e);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {// 1.获取用户Long userId = voucherOrder.getUserId();// 2.创建锁对象RLock lock = redissonClient.getLock("lock:order:" + userId);// 3.获取锁boolean isLock = lock.tryLock();// 4.若获取锁失败if (!isLock) {log.error("不允许重复下单");return;}// 获取锁成功 (理论上没有问题,lua脚本已经判断过了,这里再加锁只是兜底)try {//通过代理对象调用proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}}@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {boolean success = seckillVoucherService.update().setSql("stock = stock - 1") //使用MP,设置sql语句.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();save(voucherOrder);}private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {// 获取用户Long userId = UserHolder.getUser().getId();// 1.执行lua脚本,判断用户是否用购买资格(库存不足与重复下单问题)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,有购买资格,先创建订单,再将订单信息添加到阻塞队列VoucherOrder voucherOrder = new VoucherOrder();// 2.3 获取订单id(Redis全局唯一id)long orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);// 2.4将订单信息存入阻塞队列,任务结束orderTasks.add(voucherOrder);//3.获取代理对象,方便后序线程使用,可以放在成员变量或者是voucherOrder里面proxy = (IVoucherOrderService) AopContext.currentProxy();// 4.返回订单idreturn Result.ok(orderId);}}
相关文章:
分布式锁+秒杀异步优化
文章目录 问题思路setnx实现锁误删问题和解决方案Redis Lua脚本问题引出解决方案 setnx实现的问题Redission快速入门redission可重入锁原理 秒杀优化(异步优化)异步秒杀思路秒杀资格判断Redis消息队列 问题 比如我们两个机器都部署了我们项目,这里nginx使用轮询的方…...
数据服务化 VS 数据中台:战略演进中的价值重构
在企业数据战略的演进历程中,数据中台曾被视为解决数据孤岛的 “万能钥匙”,而数据服务化的兴起则标志着企业从 “数据资源囤积” 向 “数据价值释放” 的深刻转型。两者的核心差异不仅在于技术架构,更在于对数据资产的定位与使用理念的根本分…...
PL/SQL登录慢,程序连接Oracle 提示无法连接或无监听
PL/SQL登录慢,程序连接Oracle 提示无法连接或无监听 错误提示:ORA-12541: TNS: 无监听程序 的解决办法, 现象:PL/SQL登录慢,程序连接Oracle 提示无法连接或无监听 监听已经正常开起,但还是PL/SQL登录慢或…...
【JAVAFX】自定义FXML 文件存放的位置以及使用
情况 1:FXML 文件与调用类在同一个包中(推荐) 假设类 MainApp 的包是 com.example,且 FXML 文件放在 resources/com/example 下: 项目根目录 ├── src │ └── sample │ └── Main.java ├── src/s…...
DDoS(分布式拒绝服务)攻击
DDoS(分布式拒绝服务)攻击 这是一份全面系统的 DDoS(分布式拒绝服务攻击)知识总结,适合用于学习、报告、讲稿或者面试准备。内容涵盖定义、原理、危害、利用、工具、防护策略等。 一、什么是DDoS DDoS(Distributed Denial of Se…...
scikit-learn初探
KFold k交叉验证,k-1个作为训练集,剩下的作为测试集 split split(X, yNone, groupsNone)X: (n_samples, n_features)的矩阵,行数为n_samples,列数为n_features y:(n_samples,)为列向量,表示监…...
深入解析 sklearn 中的多种特征编码方式:功能、适用场景与选择建议
标题:深入解析 sklearn 中的多种特征编码方式:功能、适用场景与选择建议 摘要: 在机器学习中,特征编码是数据预处理的重要环节,直接影响模型的性能和效果。本文详细介绍了 sklearn 及其生态中(含第三方库…...
windows10 wsl2 安装ubuntu和docker
见 弃用Docker Desktop:在WSL2中玩转Docker之Docker Engine 部署与WSL入门-阿里云开发者社区 如果启动docker时报下面这个错, 那是因为systemctl没有启用 sudo systemctl start docker System has not been booted with systemd as init system (PID 1)…...
一文读懂WPF系列之依赖属性与附加属性
依赖属性与附加属性 依赖属性对比C#属性WPF依赖属性(Dependency Properties)优先级计算与值决策回调与验证机制WPF 自带的依赖属性自定义依赖属性 附加属性本质与定义与依赖属性的区别附加属性的典型应用场景自定义附加属性注意事项 属性…...
1×1卷积与GoogleNet
11卷积 卷积核的尺寸等于1的卷积核 11卷积有什么用 1. 通道混合与特征转换 背景:在卷积神经网络中,输入数据通常有多个通道(例如RGB图像有3个通道,经过卷积层后通道数可能会增加)。不同通道的特征图可能包含了不同的…...
Handsontable 表格组件的使用
文章目录 1. 安装 Handsontable2. 创建一个基本表格3. 主要配置3.1、 data 数据3.2、 columns 指定列配置 4. Handsontable 高级功能4.1、 添加排序4.2、 过滤数据4.3、 选中行高亮4.4、 只读单元格4.5、 校验数据 5. Handsontable 与 Vue结合6. 总结 Handsontable 是一个强大的…...
消息中间件面试题
前言 本章内容来自B站黑马程序员java大厂面试题与小林coding 如有侵权立即删除 博主学习笔记,如果有不对的地方,海涵。 如果这篇文章对你有帮助,可以点点关注,点点赞,谢谢你! 1.通用篇 1.1 什么是消息…...
数据结构与算法--1.判断数组中元素是否有重复
在C语言中,我们可以使用类似的方法来实现判断数组中是否有重复值的功能。由于C语言没有内置的哈希集合(如Python的set或C的unordered_set),我们需要自己实现一个简单的哈希表或使用其他方法。 方法一:暴力法ÿ…...
硬件工程师面试常见问题(1)
第一问:单片机上电后没有运转,首先要检查什么? (1)单片机供电是否正常& 电路焊接检查 用万用表测量对应引脚的供电电压,检查对不对。 (2)单片机复位是否释放 用万用表测量复位引…...
测试100问:web测试和APP测试的区别
哈喽,大家好,我是十二,那今天要为大家分享的是高频面试题:web测试和 App测试的区别。 从功能测试方面来讲,web测试和 App测试在测试的流程以及测试用例的设计上是没有区别的,那主要的区别包含以下三个方面&…...
Leetcode 3518. Smallest Palindromic Rearrangement II
Leetcode 3518. Smallest Palindromic Rearrangement II 1. 解题思路2. 代码实现 题目链接:Leetcode 3518. Smallest Palindromic Rearrangement II 1. 解题思路 这一题是题目Leetcode 3517. Smallest Palindromic Rearrangement I的升级版本,其主要的…...
Golang|订单相关
文章目录 秒杀写库策略确保缓存的订单数据不丢失 秒杀写库策略 在我们的抽奖函数中,抽中奖品、减库存成功返回给前端后就应该生成订单写入数据库 但是这里有问题,我们的抽奖函数是支持高并发的,并发量大的情况下mysql无法支持这么大并发量的写…...
Python+Playwright:编写自动化测试的避坑策略
PythonPlaywright:编写自动化测试的避坑策略 前言一、告别 time.sleep(),拥抱 Playwright 的智能等待二、选择健壮、面向用户的选择器,优先使用 data-testid三、严格管理环境与依赖,确保一致性四、分离测试数据与逻辑,…...
P12130 [蓝桥杯 2025 省 B] 移动距离
P12130 [蓝桥杯 2025 省 B] 移动距离 - 洛谷 题目描述 小明初始在二维平面的原点,他想前往坐标 (233, 666)。在移动过程中,他只能采用以下两种移动方式,并且这两种移动方式可以交替、不限次数地使用: 水平向右移动,…...
关于 人工智能(AI)发展简史 的详细梳理,按时间阶段划分,涵盖关键里程碑、技术突破、重要人物及挑战
以下是关于 人工智能(AI)发展简史 的详细梳理,按时间阶段划分,涵盖关键里程碑、技术突破、重要人物及挑战: 字数:约2500字 逻辑结构:时间线清晰,分阶段描述技术突破、关键事件与挑战…...
Formality:Bug记录
相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文记录博主在使用Synopsys的形式验证工具Formality中遇到的几个Bug。 Bug复现 情况一 // 例1 module dff (input clk, input d_in, output d_out …...
react-07React提交表单数据调用同一方法(高阶函数,函数柯里化概念)
1.高阶函数与函数柯里化概念 高阶函数:符合其中之一,那该函数为高阶函数 1.A函数,接收的参数是一个函数,那么A就是高阶函数2.A函数,调用的返回值是一个函数,那么A就是高阶函数 常见的高阶函数:…...
js ES6箭头函数的作用
前置知识 1、箭头函数语法简洁,相较于传统的函数表达式,箭头函数的语法更为简洁,尤其适用于简单的函数。 2、解决this取向问题,在传统函数中,this 的值取决于函数的调用方式,这可能会导致一些难以理解和调…...
淘宝商品数据高并发采集方案:API 接口限流机制与分布式调用实战
一、引言 在电商领域,对淘宝商品数据进行采集是一项常见且重要的任务。随着业务规模的扩大,高并发采集需求日益凸显。然而,淘宝 API 接口存在限流机制,为了高效且合规地完成数据采集,需要采用分布式调用的策略。本文将…...
python爬虫 线程,进程,协程
0x00 线程 线程是一个执行单位,是在一个进程里面的,是共享进程里面的提供的内存等资源,使用多个线程时和使用多个进程相比,多个线程使用的内存等资源较少。进程像一座“房子”(独立资源),线程是…...
《忘尘谷》音阶与调性解析
一、音高与音名的对应关系 根据搜索结果及音乐理论,结合《忘尘谷》的曲谱信息,其音阶与调性分析如下: 调性判定 原曲调性为 D调(原曲标注为D调),但曲谱编配时采用 C调指法,通过变调夹夹2品&…...
实验一 HDFS的Shell操作
一、实验目的 熟悉HDFS Shell的语法格式,完成对HDFS上文件和目录的一系列操作 二、实验要求 2.1 练习dfs常用的子命令 ① -ls ② -du ③ -mv ④ -cp ⑤ -rm ⑥ -put ⑦ -cat ⑧ -help ⑨ -mkdir ⑩ -get 2.2通过Shell脚本定时采集数据到HDFS 三、实…...
安装SQLServer管理工具
1.回到安装SQLServer的页面,选择安装SQLServer的管理工具2.跳转官网下载 3.安装SSMS 4.安装中5.安装成功以后打开软件,输入信息连接数据库 也可以在本机通过证书链接选择Windows身份验证,就不需要输入账号密码,但只能在安装sql…...
从JSON到SQL:基于业务场景的SQL生成器实战
引言 在数据驱动的业务场景中,将业务需求快速转化为SQL查询是常见需求。本文将通过一个轻量级的sql_json_to_sql函数,展示如何将JSON格式的查询描述转换为标准SQL语句,并结合实际业务场景验证其功能。 核心代码解析 1. 代码实现 def sql_j…...
记录鸿蒙应用上架应用未配置图标的前景图和后景图标准要求尺寸1024px*1024px和标准要求尺寸1024px*1024px
审核报错【①应用未配置图标的前景图和后景图,标准要求尺寸1024px*1024px且需下载HUAWEI DevEco Studio 5.0.5.315或以上版本进行图标再处理、②应用在展开状态下存在页面左边距过大的问题, 应用在展开状态下存在页面右边距过大的问题, 当前页面左边距: 504 px, 当前页面右边距…...
蓝桥杯嵌入式十六届赛前复习总结与准备
一.软件使用 赛点是没有网络的,要自己下载原件与数据包,这里给大家一个演示 在updater Settings这里设置文件存放位置,为了方便查找和提交文件,建议在桌面建立一个文件夹来存放。 把赛点的芯片包复制到创建的文件夹然后解压缩 之…...
了解一下Unity的RenderQueue
在Unity中,场景里的每个物体都需要通过渲染管线绘制到屏幕上。渲染管线处理光照、材质、纹理等信息,最终决定物体的显示效果。但当场景中有多个物体时,它们的绘制顺序会直接影响画面结果,尤其是在涉及透明物体或特效时。这时&…...
使用CS Roofline Toolkit测量带宽
使用CS Roofline Toolkit测量带宽 工程下载:使用CS Roofline Toolkit测量带宽-案例工程文件,也可以按照下面的说明使用git clone下载 目录 使用CS Roofline Toolkit测量带宽0、Roofline模型理解1、CS Roofline Toolkit下载1.1、设置代理1.2、git clone下…...
第三篇:深入 Framer Motion Variants:掌握组件动画编排的艺术
🎯 前言 在动态交互主导的现代前端开发中,优雅的动画效果已成为提升用户体验的重要元素。Framer Motion 的 Variants(动画变体)功能,通过状态化管理和动画编排能力,让复杂动效的实现变得前所未有的高效。本…...
狂神SQL学习笔记四:基本的命令行操作
注:所有语句用 ; 结尾,– 单行注释,/* 多行注释 */ 连接数据库 mysql -uroot -p123456 --连接数据库查看所有的数据库 切换数据库 查看数据库中所有的表 显示数据库中所有表的信息 创建一个数据库 退出连接...
架构思维:缓存层场景实战_读缓存(下)
文章目录 Pre业务场景缓存存储数据的时机与常见问题解决方案1. 缓存读取与存储逻辑2. 高并发下的缓存问题及解决方案3. 缓存预热(减少冷启动问题) 缓存更新策略(双写问题)1. 先更新缓存,再更新数据库(不推荐…...
软件架构设计:MVC、MVP、MVVM、RIA 四大风格优劣剖析
MVC、MVP、MVVM 和 RIA 都是软件架构中常见的设计风格,以下是对它们的详细介绍: 一、MVC 架构风格(Model - View - Controller) 1.简介:MVC 架构风格将软件应用程序分为三个核心部分,通过这种划分来分离不…...
java基础课程-springmvc课程
一. 回顾MVC: tomcat是servlet容器, servlet实现方式: xml中配置: 二. 回顾servlet: 三. SpringMvc学习 3.1 springMvc搭建和讲解: jar包引入spring-webmvc即可。 核心:DispatcherServlet Spring的web…...
NLP高频面试题(四十二)——RAG系统评估:方法、指标与实践指南
1. 引言:RAG系统概述与评估挑战 检索增强生成(Retrieval-Augmented Generation,简称 RAG)是近年来自然语言处理领域的一个重要进展。RAG系统在大型语言模型生成文本的过程中引入了外部检索模块,从外部知识库获取相关信息,以缓解纯生成模型可能出现的幻觉和知识盲点。通过…...
Flutter学习 滚动组件(1):ListView基本使用
目录 一、ListView构造方法1.1 常规方法1.2 ListView.builder1.3 ListView.separated 二、自定义ListView样式和布局:三、ListView性能优化:总结: 一、ListView构造方法 主要以下几种方法: 常规方法,直接使用默认的构…...
处理 Flutter 没有反应
现象 有以下几种 VS Code 中 Initializing the Flutter SDK. This may take a few minutes. 会一直维持在这个右下角提示窗, 但是无后续动作 Flutter CMD flutter_console.bat 执行 --version 或者 doctor [-v] 没有任何输出, 命令卡住 解决办法 参考官方说明 管理员身份…...
java面向对象06:封装
封装 该露的露,该藏的藏 我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用。 封装(数据的隐藏) 通常&#x…...
测试定时发布
测试定时发布 测试定时发布 测试定时发布 测试定时发布 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 新的改变 我…...
2025年4月15日 百度一面 面经
目录 1. 代理相关 从静态代理到动态代理 2. cglib可以代理被final修饰的类吗,为什么 3. JVM 体系结构 4. 垃圾回收算法 5. 什么是注解 如何使用 底层原理 6. synchronized和reentrantlock 7. 讲一下你项目中 redis的分布式锁 与java自带的锁有啥区别 8. post 请求和 ge…...
抖音卸载了ip属地还会更新吗?深度解析
近期,关于抖音IP属地显示功能的讨论热度持续攀升,许多用户提出疑问:如果卸载抖音APP,账号的IP属地还会继续更新吗?这一问题背后,既涉及平台算法的运作逻辑,也关乎用户对隐私保护的深层需求。本文…...
Spring IoC与DI详解:从Bean概念到手写实现
一、Spring Bean的概念与本质 1.1 什么是Bean? 在Spring框架中,Bean是一个由Spring IoC容器实例化、组装和管理的对象。Bean及其之间的依赖关系通过容器使用的配置元数据来定义。简单来说,Bean就是Spring容器管理的Java对象。简单来说&…...
【Qt】信号和槽
文章目录 信号和槽的概念信号和槽的使用链接信号和槽查看内置信号和槽通过 Qt Creator 生成信号槽代码 自定义信号和槽基本语法带参数的信号和槽 信号与槽的连接方式一对一一对多多对一 信号和槽的其他说明信号与槽的断开 使用 Lambda 表达式定义槽函数 信号和槽的概念 在 Qt 中…...
小事务架构下的业务完整性保障:基于业务处理记录与补偿机制的技术实现
随着微服务架构、事件驱动架构(EDA)和最终一致性理念的普及,传统的大事务管理方式被更细粒度的“小事务”所取代。在这种架构中,全局业务流程被拆解成多个局部事务节点,通过异步消息进行编排。这种解耦提高了可扩展性和…...
DELL电脑开机进入自检界面
疑难解答 - 如何解决开机直接进入BIOS画面 添加链接描述 一、DELL电脑开机自检提示please run setup program 未设置一天中的时间-请运行安装程序(Time-of-day not set - please run SETUP program) 配置信息无效-请运行安装程序(Invalid configuration information - ple…...
Spring Boot 微服务中集成 MyBatis-Plus 与集成原生 MyBatis 有哪些配置上的不同?
在Spring Boot 微服务中集成 MyBatis-Plus (MP) 与集成原生 MyBatis (MB) 在配置上的主要不同点。MyBatis-Plus 是在 MyBatis 基础上进行的增强,它兼容 MyBatis 的所有配置方式,并提供了更简洁、更强大的配置选项。 相同点: 基础数据源配置 (DataSource…...