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

Redis 学习笔记 4:优惠券秒杀

Redis 学习笔记 4:优惠券秒杀

本文基于前文的黑马点评项目进行学习。

Redis 生成全局唯一ID

整个全局唯一 ID 的结构如下:

image-20240222153930341

这里的时间戳是当前时间基于某一个基准时间(项目开始前的某个时间点)的时间戳。序列号是依赖 Redis 生成的对于某个业务唯一的自增量。

先计算一个基准时间的时间戳:

public static void main(String[] args) {// 计算时间戳基准时间戳long baseTimestamp = LocalDateTime.of(2024, 1, 1, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);System.out.println(baseTimestamp);
}

这里我选择的基准时间是 2024年1月1日0点0分0秒。

编写生成全局唯一 ID 的工具类:

@Component
public class GlobalIdGenerator {private static final long BASE_TIMESTAMP = 1704067200L;@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 生成全局唯一ID** @param key 业务代码* @return 全局唯一ID*/public long genGlobalId(String key) {// 计算时间戳差额LocalDateTime now = LocalDateTime.now();long timestamp = now.toEpochSecond(ZoneOffset.UTC) - BASE_TIMESTAMP;// 从 Redis 获取对应的自增量String dateStr = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));String redisKey = "incr:" + key + ":" + dateStr;long incrNum = stringRedisTemplate.opsForValue().increment(redisKey);// 生成唯一idreturn timestamp << 32 | incrNum;}
}

这里的BASE_TIMESTAMP值取之前设置的基准时间的时间戳。生成 ID 的算法是将时间戳差值(timestamp)左移 32 位后再与序列号(incrNum)按位或。

编写测试用例:

@SpringBootTest
public class GlobalIdGeneratorTests {@Autowiredprivate GlobalIdGenerator globalIdGenerator;@Testpublic void test() throws InterruptedException {final int THREAD_NUM = 30;CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUM);ExecutorService es = Executors.newFixedThreadPool(THREAD_NUM);Runnable task = () -> {for (int i = 0; i < 1000; i++) {long orderID = globalIdGenerator.genGlobalId("order");System.out.println(orderID);}countDownLatch.countDown();};LocalDateTime begin = LocalDateTime.now();for (int i = 0; i < THREAD_NUM; i++) {es.execute(task);}countDownLatch.await();LocalDateTime end = LocalDateTime.now();long millis = Duration.between(begin, end).toMillis();System.out.println("共耗时" + millis + "毫秒");}
}

这里用 30 个线程生成 ID,每个线程执行 1000 次生成过程,共生成 30000 次 ID。

添加优惠券

通过接口添加秒杀优惠券,调用示例可以参考这里。

优惠券下单

package com.hmdp.service.impl;//.../*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@AutowiredISeckillVoucherService seckillVoucherService;@AutowiredGlobalIdGenerator globalIdGenerator;/*** 创建优惠券订单** @param voucherId* @return*/@Transactional@Overridepublic Result createOrder(Long voucherId) {// 获取秒杀优惠券信息SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 判断当前时间是否在优惠券有效期内LocalDateTime now = LocalDateTime.now();if (now.isBefore(voucher.getBeginTime())) {return Result.fail("优惠券抢购未开始");}if (now.isAfter(voucher.getEndTime())) {return Result.fail("优惠券抢购已结束");}// 判断优惠券库存是否够if (voucher.getStock() <= 0) {return Result.fail("缺少库存");}// 扣减库存boolean res = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();if (!res){return Result.fail("缺少库存");}// 生成秒杀优惠券订单VoucherOrder voucherOrder = new VoucherOrder();voucherOrder.setId(globalIdGenerator.genGlobalId("voucher-order"));voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(UserHolder.getUser().getId());this.save(voucherOrder);return Result.ok(voucherOrder.getId());}
}

优惠券秒杀

超卖问题

上面的代码在多个线程并发抢购的情况下,会出现超卖问题。

我们可以用 JMeter 来模拟多线程抢购的情况,测试脚本可以从下面的链接下载:

通过网盘分享的文件:秒杀抢购.jmx
链接: https://pan.baidu.com/s/1BmKKhN1FCLJH_qR7hXB0Uw?pwd=y74i 提取码: y74i

需要注意的是,在导入测试脚本后,需要在登录状态头中填入当前登录用户的token以模拟用户登录:

image-20250502170821227

可以修改 Redis 中的 token 过期时间以保持长时间有效登录,方便后续测试。

此外还需要将测试接口中的路径参数修改为你要模拟抢购的目标优惠券id:

image-20250502170951955

为了方便观察,在抢购前将优惠券库存修改为100

image-20250502171115120

并清空tb_voucher_order表。

执行 JMeter 的秒杀测试脚本,可以观察到库存为负:

image-20250502171328600

tb_voucher_order表创建了109个订单。

查看 JMeter 测试报告可以看到有 70% 左右的调用失败:

image-20250502171502840

同步锁

以上问题是因为在多线程并发的情况下,优惠券的库存资源是多个线程的共享资源,事实上每个线程的读取和写入库存动作是分步完成的,所以可能在读取和写入动作之间,其它线程完成读取/写入动作,就会导致出现多线程的数据一致性问题。

同样的,像 Java 解决多线程问题那样,用互斥锁可以解决此类问题:

// ...
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {// ...private final Object orderCreateLock = new Object[0];@Synchronized("orderCreateLock")@Transactional@Overridepublic Result createOrder(Long voucherId) {// ...if (voucher.getStock() <= 1) {return Result.fail("缺少库存");}// ...}
}
  • @Synchronized注解的用途可以看这里。
  • 判断库存这里必须是<=1而非<=0,否则会超卖掉1个优惠券,原因可能与 MyBatis 缓存有关。

现在执行 JMeter 就不会出现超卖的情况了。

乐观锁

上面的互斥锁实际上是一种“悲观锁”,即认为并发情况下出现问题是必然且高频的,所以必须对访问共享数据的代码进行锁保护,以让这些代码串行执行。对应的,还有一种“乐观锁”,即认为这种情况是低频的,在某种程度上可以容许它们发生,只需要在关键的更新数据时进行检查,如果有其他线程已经对数据进行了修改,就放弃当前线程对数据的修改并返回错误,这样同样不会导致共享数据出错。

最简单的方式是在存在共享数据的表中添加一个version字段,以用于乐观锁检查是否有其他线程修改数据。在这个示例中,我们需要给tb_seckill_voucher表添加一个version字段:

`version` bigint NOT NULL COMMENT '用于乐观锁的版本控制',

给实体类添加相应字段:

public class SeckillVoucher implements Serializable {// ...private Long version;
}

在扣减库存时检查 version 字段:

// ...
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {// ...@Transactional@Overridepublic Result createOrder(Long voucherId) {// ...// 扣减库存时检查 versionboolean res = seckillVoucherService.update().setSql("stock=stock-1,version=version+1").eq("voucher_id", voucherId).eq("version", voucher.getVersion()).update();if (!res) {return Result.fail("缺少库存");}// ...}
}

MybatisPlus 有一个 @version注解可以自动处理乐观锁的veresion字段,但不如自己控制灵活。

这里在正常更新库存后,version 字段会自增,所以一旦当前线程在更新时发现 version 字段匹配不到,就说明其它线程已经修改了库存。

对于当前这个示例,version 字段实际上并不是必须的,完全可以用库存字段代替,换言之,在更新库存时进行判断,如果库存与读取时一致,说明其它线程还没有更新库存,可以更新,反之则不可。

删除表和实体中的 version 字段后代码修改为:

// 扣减库存时检查 version
boolean res = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).eq("stock", voucher.getStock()).update();

此时执行测试脚本会发现实际上 200 个线程中有 80% 都会失败,查看优惠券库存也会发现只扣减掉 20 个库存。

这是因为相比悲观锁的串行执行,这里乐观锁下会有大量线程执行到提交 update 语句时才发现库存已经改变,继而放弃更新。但事实上,在这个示例中,只要库存大于0,即使库存已经被其它线程修改,也是可以提交更新的。也就是说在这里可以放宽乐观锁的更新条件:

// 扣减库存时检查
boolean res = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0) // 只要库存大于0都可以提交更新.update();

再次执行测试,就会发现200个线程有50%的错误率,库存也能正确扣减到0。

一人一单

假设我们需要给优惠券抢购添加限制,一人只能抢购一单:

// 检查用户是否已经抢购过该优惠券
Integer count = this.query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
if (count > 0) {return Result.fail("已经抢购过优惠券,不能重复抢购");
}
// 扣减库存时检查
// ...

实际运行测试会发现依然会出现一个用户抢购多单的情况,其原因和之前并发情况下扣减库存是相同的——同一时间有多个线程“越过”了检查点并扣减了库存。

与扣减库存所不同的是,这里检查的是表数据中是否存在特定数据行,而非某个数据的修改,所以无法通过版本控制的方式使用乐观锁。但悲观锁依然是有效的:

// ...
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {// ...@Overridepublic Result createOrder(Long voucherId) {// ...return doCreateOrder(voucherId);}@Transactionalpublic synchronized Result doCreateOrder(Long voucherId) {// 检查用户是否已经抢购过该优惠券Integer count = this.query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("已经抢购过优惠券,不能重复抢购");}// 扣减库存时检查// ...// 生成秒杀优惠券订单// ...}
}

为了方便起见,这里将需要加锁的部分(涉及优惠券订单表的操作)封装到单独方法doCreateOrder中。且因为createOrder方法中仅包含数据读取操作,所以将事务缩小到doCreateOrder方法。

事实上上面的代码是有问题的,因为 Spring 的事务是通过 AOP 实现的,而 AOP 又是通过代理对象实现的。这导致 Spring 的默认代理存在一个自调用会导致 AOP 失效的问题。换言之,这里的createOrder方法调用this.doCreateOrder(...)不会使用事务,也就是常说的事务失效的情况之一。

可以通过开启相关日志的方式观察 JDBC 事务有没有在 SQL 执行时开启:

logging:level:org:springframework:jdbc:datasource:DataSourceTransactionManager: DEBUGtransaction:interceptor: TRACE

解决这个问题有多种方式,这里通过最简单的显式获取代理对象并调用的方式:

// ...
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {// ...@Autowiredprivate ApplicationContext applicationContext;// ...@Overridepublic Result createOrder(Long voucherId) {// 显式获取代理对象并调用VoucherOrderServiceImpl proxy = applicationContext.getBean(VoucherOrderServiceImpl.class);return proxy.doCreateOrder(voucherId);}@Transactionalpublic synchronized Result doCreateOrder(Long voucherId) {// ...}
}

目前代码依然存在问题,运行测试就会发现,有小概率会出现一个用户抢了同一个优惠券2次以上的情况出现。

仔细分析代码,在实际 Spring 框架执行时,代码大概是这样的:

代理类{用于执行 JDBC 事务的方法(){开启 JDBC 事务;调用实际上的业务方法;结束 JDBC 事务;}
}

而我们的锁是加在实际上的业务代码(具体是这里的doCreateOrder)方法上的,换言之,锁保护的临界区范围是小于 JDBC 事务范围的,这就导致这么一个问题——某个线程的事务还没有提交,订单表中的数据还没有生成,锁就已经释放,此时其它线程就可能获取锁并执行订单创建。

所以正确的方式是让锁完整包裹事务

// ...
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {// ...@Overridepublic Result createOrder(Long voucherId) {// ...synchronized (this){return proxy.doCreateOrder(voucherId);}}@Transactionalpublic Result doCreateOrder(Long voucherId) {// ...}
}

这样就 OK 了。

实际上这里并不需要对this加锁,因为我们仅需要避免针对同一个用户的并发,不同用户是不需要考虑并发情况的。所以使用用户标识加锁可以有效改善并发性能:

Long userId = UserHolder.getUser().getId();
// 使用用户标识进行加锁
synchronized (userId.toString().intern()){return proxy.doCreateOrder(voucherId);
}

注意,这里使用的是userId.toString().intern()而非userId.toString(),前者会获取同一个内容字符串的字符串常量池中的引用。

分布式锁

上面展示的仅是单体应用的情况下如何解决并发问题,如果是分布式应用(存在多个JVM),之前使用的锁就不再有效,因为这些锁仅在 JVM 内生效,可以保证其内部的线程“互斥执行”,但对于 JVM 之间,这些锁将不再有效。

此时我们需要使用 **JVM 外的锁(即分布式锁)**来保证不同 JVM 的线程也能够互斥执行。

常见的分布式锁有三种实现方式:

image-20250503143346533

这里使用 Redis 实现分布式锁,需要特别注意的是,当特殊情况下线程在获取锁后,释放锁前意外终止时,Redis 锁并不会自动释放,因此需要利用 Redis 的过期时间来防止这种情况下导致的死锁

原理

基于 Redis 的分布式锁实现实际是利用 setnx 命令实现的:

127.0.0.1:6379> setnx lock 123
(integer) 1
127.0.0.1:6379> setnx lock 123
(integer) 0
127.0.0.1:6379> keys lock
1) "lock"

释放锁即删除对应的 key:

127.0.0.1:6379> del lock
(integer) 1
127.0.0.1:6379> setnx lock 123
(integer) 1
127.0.0.1:6379> setnx lock 123
(integer) 0

正如之前说的,为了避免特殊情况下的死锁,需要在获取锁后指定一个有效时间:

127.0.0.1:6379> setnx lock 123
(integer) 1
127.0.0.1:6379> EXPIRE lock 10
(integer) 1
127.0.0.1:6379> ttl lock
(integer) 5
127.0.0.1:6379> ttl lock
(integer) 3

需要注意的是,获取锁和指定过期时间的命令应当“一起执行”,即要确保其具备原子性,以防止获取锁后,设置过期时间之前线程以外终止导致的死锁。

因此应当使用以下命令:

127.0.0.1:6379> SET lock 123 ex 10 nx
OK
127.0.0.1:6379> ttl lock
(integer) -2

实现

在实现分布式锁前,先将示例应用修改为分布式,即启动两个实例:

image-20250503144708235

修改 Nginx 配置,启用反向代理和负载均衡:

# ...
http {# ...server {# ...location /api {  # ...  # proxy_pass http://127.0.0.1:8081;proxy_pass http://backend;}}upstream backend {server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;}  
}

重启 Nginx:

❯ nginx -s quit
❯ start nginx

可以通过开启两个标签页分别抢购,并添加断点的方式观察两个不同 JVM 的线程同时进入加锁的临界区的情况:

image-20250503150143027

创建分布式锁接口:

public interface ILock {/*** 尝试获取锁(非阻塞式)** @param timeoutSec 超时自动释放(单位,秒)* @return*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}

创建实现类:

public class SimpleRedisLock implements ILock {private StringRedisTemplate stringRedisTemplate;// 用于锁的业务名称private final String businessName;// 锁的统一前缀private static final String KEY_PREFIX = "lock:";// redis keyprivate final String redisKey;public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String businessName) {this.stringRedisTemplate = stringRedisTemplate;this.businessName = businessName;this.redisKey = KEY_PREFIX + businessName;}@Overridepublic boolean tryLock(long timeoutSec) {String threadName = Thread.currentThread().getName();Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, threadName, timeoutSec, TimeUnit.SECONDS);return BooleanUtil.isTrue(isLock);}@Overridepublic void unlock() {stringRedisTemplate.delete(redisKey);}
}

使用分布式锁保护临界区:

// 使用用户标识进行加锁
ILock lock = new SimpleRedisLock(stringRedisTemplate, "voucher-order:" + userId.toString());
boolean isLock = lock.tryLock(2000);
if (!isLock){return Result.fail("同一用户不能重复抢购");
}
try{return proxy.doCreateOrder(voucherId);
}
finally {lock.unlock();
}

方便起见,这里是通过非阻塞的方式用 Redis 加锁,如果获取失败,就简单返回错误。毕竟这里锁的颗粒度是针对单个用户,只有一个用户同时有大量请求时才会触发失败,此种情况下可以认为用户在用脚本秒杀。

为了用 debug 的方式观察分布式锁的生效,这里给了一个较长的锁过期时间(2000秒),在真实开发中,通常只需要设定一个比业务执行时长稍长的时间,比如业务平均执行时长的10倍。

锁误删

上面的 “Redis 锁” 实际上存在一个问题:

image-20250503190910388

就像上图展示的,如果线程1因为某些原因长时间阻塞,此时 Redis 锁因为超时被释放,然后其它线程(线程2)就可以获取 Redis 锁。而稍后在线程1执行完业务代码后,会释放 Redis 锁,而此时线程2可能仍然在执行,释放了锁后别的线程(线程3)就可以获取锁。在这种情况下,两个线程(线程2和线程3)同时获取了锁,并同时在执行业务代码。

这个问题的本质是当前线程释放了其它线程获取的锁而导致的。因此,解决该问题的关键在于,释放 Redis 锁时要进行检查,即只能释放自己获取的锁

修改 Redis 锁的代码,添加相应验证逻辑:

public class SimpleRedisLock implements ILock {private StringRedisTemplate stringRedisTemplate;// 用于锁的业务名称private final String businessName;// 锁的统一前缀private static final String KEY_PREFIX = "lock:";// redis keyprivate final String redisKey;// uuid,用于区分不同 JVM 创建的锁private static final String uuid = UUID.randomUUID().toString(true);public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String businessName) {this.stringRedisTemplate = stringRedisTemplate;this.businessName = businessName;this.redisKey = KEY_PREFIX + businessName;}@Overridepublic boolean tryLock(long timeoutSec) {final String jvmThreadId = getJvmThreadId();Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, jvmThreadId, timeoutSec, TimeUnit.SECONDS);return BooleanUtil.isTrue(isLock);}/*** 获取锁位于JVM当前线程的唯一标识** @return 锁位于JVM当前线程的唯一标识*/private String getJvmThreadId() {// 获取当前线程id,用于区分同一个 JVM 的不同线程创建的锁long threadId = Thread.currentThread().getId();// 拼接 uuid和线程id,可以唯一确定一个JVM中的一个线程final String jvmThreadId = uuid + "-" + threadId;return jvmThreadId;}@Overridepublic void unlock() {// 删除 Redis key 时判断,只删除当前线程创建的 key// 从 redis 获取锁对应的值String lockVal = stringRedisTemplate.opsForValue().get(redisKey);String jvmThreadId = getJvmThreadId();// 比较值与当前 JVM-线程标识是否一致if (jvmThreadId.equals(lockVal)){stringRedisTemplate.delete(redisKey);}}
}

Lua脚本

在极端情况下,上面的代码依然会出错:

image-20250503202634493

原因是检查锁是否是自己获取的与释放锁操作是分别执行的,这两者之间依然可能出现阻塞进而导致之前所说的问题。要解决这个问题需要借助 Lua 脚本。

Redis 是支持执行 Lua 脚本的,并且可以保证执行单个 Lua 脚本时是原子性的。换言之,我们可以借助 Lua 脚本来批量地执行一些 Redis 命令,以保证这些命令执行时的原子性。

先编写用于释放锁的 Lua 脚本:

-- Redis 锁删除脚本,对比 Redis 中的锁标识与当前给定标识,如果一致,删除,否则不删除
------------------
-- 从命令行获取参数
-- Redis 锁对应的key
local lockKey = KEYS[1]
-- Redis 锁需要匹配的目标标识
local targetKeyVal = ARGV[1]
-- 获取 Redis 中存储的锁的当前值
local keyVal = redis.call('get', lockKey)
if (targetKeyVal == keyVal) then-- 删除锁对应的keyreturn redis.call('del', lockKey)
end
-- 失败返回
return 0
  • 这里的 Lua 脚本位于resources/unlock.lua
  • 更多的 Lua 语法可以参考Lua 教程 | 菜鸟教程

释放 Redis 锁的操作改为通过执行脚本完成,以确保其原子性:

// ...
public class SimpleRedisLock implements ILock {// ...// 用 Lua 编写的 Redis 锁释放脚本private static final DefaultRedisScript<Long> REDIS_SCRIPT;static {REDIS_SCRIPT = new DefaultRedisScript<>();// 指定脚本的位置REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));// 指定脚本的返回值类型REDIS_SCRIPT.setResultType(Long.class);}// ...@Overridepublic void unlock() {// 使用 lua 脚本删除 Redis 锁List<String> keys = Collections.singletonList(redisKey);String jvmThreadId = getJvmThreadId();stringRedisTemplate.execute(REDIS_SCRIPT, keys, jvmThreadId);}
}

本文的示例代码可以从这里获取。

参考资料

  • 黑马程序员Redis入门到实战教程

相关文章:

Redis 学习笔记 4:优惠券秒杀

Redis 学习笔记 4&#xff1a;优惠券秒杀 本文基于前文的黑马点评项目进行学习。 Redis 生成全局唯一ID 整个全局唯一 ID 的结构如下&#xff1a; 这里的时间戳是当前时间基于某一个基准时间&#xff08;项目开始前的某个时间点&#xff09;的时间戳。序列号是依赖 Redis 生…...

C++学习:六个月从基础到就业——C++17:if/switch初始化语句

C学习&#xff1a;六个月从基础到就业——C17&#xff1a;if/switch初始化语句 本文是我C学习之旅系列的第四十六篇技术文章&#xff0c;也是第三阶段"现代C特性"的第八篇&#xff0c;主要介绍C17引入的if和switch语句的初始化表达式特性。查看完整系列目录了解更多内…...

C++跨平台开发经验与解决方案

在当今软件开发领域&#xff0c;跨平台开发已成为一个重要的需求。C作为一种强大的系统级编程语言&#xff0c;在跨平台开发中扮演着重要角色。本文将分享在实际项目中的跨平台开发经验和解决方案。 1. 构建系统选择 CMake的优势 跨平台兼容性好 支持多种编译器和IDE 强大…...

RabbitMQ 工作模式(上)

前言 在 RabbitMQ 中&#xff0c;一共有七种工作模式&#xff0c;我们也可以打开官网了解&#xff1a; 本章我们先介绍前三种工作模式 &#xff08;Simple&#xff09;简单模式 P&#xff1a;producer 生产者&#xff0c;负责发送消息 C&#xff1a;consumer 消费者&#x…...

为什么需要加密机服务?

前言 大家好&#xff0c;我是老马。 以前我自己在写工具的时候&#xff0c;都是直接自己实现就完事了。 但是在大公司&#xff0c;或者说随着合规监管的要求&#xff0c;自己随手写的加解密之类的&#xff0c;严格说是不合规的。 作为一家技术性公司&#xff0c;特别是金融…...

【Linux】利用多路转接epoll机制、ET模式,基于Reactor设计模式实现

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f152; C 语言 | &#x1f310; 计算机网络 上篇文章&#xff1a;多路转接epoll&#xff0c;实现echoserver 至此&#xff0c;Linux与…...

c/c++的findcontours崩溃解决方案

解决 Windows 平台 OpenCV findContours 崩溃&#xff1a;一种更稳定的方法 许多在 Windows 平台上使用 OpenCV 的开发者可能会在使用 findContours 函数时&#xff0c;遇到令人头疼的程序崩溃问题。尽管网络上流传着多种解决方案&#xff0c;但它们并非总能根治此问题。 当时…...

机器学习 Day18 Support Vector Machine ——最优美的机器学习算法

1.问题导入&#xff1a; 2.SVM定义和一些最优化理论 2.1SVM中的定义 2.1.1 定义 SVM 定义&#xff1a;SVM&#xff08;Support Vector Machine&#xff0c;支持向量机&#xff09;核心是寻找超平面将样本分成两类且间隔最大 。它功能多样&#xff0c;可用于线性或非线性分类…...

npm与pnpm--为什么推荐pnpm

包管理器中 npm是最经典的&#xff0c;但大家都任意忽略一个更优质的管理器&#xff1a;pnpm 1. 核心区别 特性npmpnpm依赖存储方式扁平化结构&#xff08;可能重复依赖&#xff09;硬链接 符号链接&#xff08;共享依赖&#xff0c;节省空间&#xff09;安装速度较慢&#…...

ollama调用千问2.5-vl视频图片UI界面小程序分享

1、问题描述&#xff1a; ollama调用千问2.5-vl视频图片内容&#xff0c;通常用命令行工具不方便&#xff0c;于是做了一个python UI界面与大家分享。需要提前安装ollama&#xff0c;并下载千问qwen2.5vl:7b 模型&#xff0c;在ollama官网即可下载。 &#xff08;8G-6G 显卡可…...

济南国网数字化培训班学习笔记-第三组-1-电力通信传输网认知

电力通信传输网认知 电力通信基本情况 传输介质 传输介质类型&#xff08;导引与非导引&#xff09; 导引传输介质&#xff0c;如电缆、光纤&#xff1b; 非导引传输介质&#xff0c;如无线电波&#xff1b; 传输介质的选择影响信号传输质量 信号传输模式&#xff08;单工…...

Kubernetes控制平面组件:Kubelet详解(六):pod sandbox(pause)容器

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…...

51单片机,两路倒计时,LCD1602 ,Proteus仿真

初始上电 默认2路都是0分钟的倒计时 8个按键 4个一组 一组控制一路倒计时 4个 按键:加 减 开始或者暂停 复位到0分钟相当于停止 针对第一路倒计时 4个 按键2:加 减 开始或者暂停 复位到0分钟相当于停止 针对第2路倒计时 哪一路到了0后蜂鸣器响 对应LED点亮 main.c 文件实现了…...

MySQL之储存引擎和视图

一、储存引擎 基本介绍&#xff1a; 1、MySQL的表类型由储存引擎(Storage Engines)决定&#xff0c;主要包括MyISAM、innoDB、Memory等。 2、MySQL数据表主要支持六种类型&#xff0c;分别是&#xff1a;CSV、Memory、ARCHIVE、MRG_MYISAN、MYISAM、InnoBDB。 3、这六种又分…...

写spark程序数据计算( 数据库的计算,求和,汇总之类的)连接mysql数据库,写入计算结果

1. 添加依赖 在项目的 pom.xml&#xff08;Maven&#xff09;中添加以下依赖&#xff1a; xml <!-- Spark SQL --> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql_2.12</artifactId> <version>3.3.0…...

一:操作系统之系统调用

系统调用&#xff1a;用户程序与操作系统交互的桥梁 在计算机的世界里&#xff0c;应用程序是我们日常接触最多的部分&#xff0c;比如浏览器、文本编辑器、游戏等等。然而&#xff0c;这些应用程序并不能直接控制硬件资源&#xff0c;比如读写硬盘、创建新进程、发送网络数据…...

【ROS2】 核心概念6——通信接口语法(Interfaces)

古月21讲/2.6_通信接口 官方文档&#xff1a;Interfaces — ROS 2 Documentation: Humble documentation 官方接口代码实战&#xff1a;https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Single-Package-Define-And-Use-Interface.html ROS 2使用简化的描…...

SmartETL函数式组件的设计与应用

SmartETL框架主要采用了面向对象的设计思想&#xff0c;将ETL过程中的处理逻辑抽象为Loader和Processor&#xff08;对应loader模块和iterator模块&#xff09;&#xff0c;所有流程组件需要继承或实现DataProvider&#xff08;iter方法&#xff09;或JsonIterator&#xff08;…...

Spring Security与SaToken的对比与优缺点分析

Spring Security与SaToken对比分析 一、框架定位 Spring Security 企业级安全解决方案&#xff0c;深度集成Spring生态提供完整的安全控制链&#xff08;认证、授权、会话管理、攻击防护&#xff09;适合中大型分布式系统 SaToken 轻量级权限认证框架&#xff0c;专注Token会…...

|从零开始的Pyside2界面编程| 环境搭建以及第一个ui界面

&#x1f411; |从零开始的Pyside2界面编程| 环境搭建以及第一个ui界面&#x1f411; 文章目录 &#x1f411; |从零开始的Pyside2界面编程| 环境搭建以及第一个ui界面&#x1f411;♈前言♈♈Pyside2环境搭建♈♈做个简单的UI界面♈♒代码实现♒♒QTdesigner设计UI界面♒ ♒总…...

【爬虫】DrissionPage-7

官方文档&#xff1a; https://www.drissionpage.cn/browser_control/get_page_info/ 1. 页面信息 &#x1f4cc; html 描述&#xff1a;返回当前页面的 HTML 文本。注意&#xff1a;不包含 <iframe> 元素的内容。返回类型&#xff1a;str 示例&#xff1a; html_co…...

系统架构设计(十二):统一过程模型(RUP)

简介 RUP 是由 IBM Rational 公司提出的一种 面向对象的软件工程过程模型&#xff0c;以 UML 为建模语言&#xff0c;是一种 以用例为驱动、以架构为中心、迭代式、增量开发的过程模型。 三大特征 特征说明以用例为驱动&#xff08;Use Case Driven&#xff09;需求分析和测…...

深入解析Java事件监听机制与应用

Java事件监听机制详解 一、事件监听模型组成 事件源&#xff08;Event Source&#xff09; 产生事件的对象&#xff08;如按钮、文本框等组件&#xff09; 事件对象&#xff08;Event Object&#xff09; 封装事件信息的对象&#xff08;如ActionEvent包含事件源信息&#xf…...

QT聊天项目DAY11

1. 验证码服务 1.1 用npm安装redis npm install redis 1.2 修改config.json配置文件 1.3 新建redis.js const config_module require(./config) const Redis require("ioredis");// 创建Redis客户端实例 const RedisCli new Redis({host: config_module.redis_…...

Python训练营---Day29

知识点回顾 类的装饰器装饰器思想的进一步理解&#xff1a;外部修改、动态类方法的定义&#xff1a;内部定义和外部定义 作业&#xff1a;复习类和函数的知识点&#xff0c;写下自己过去29天的学习心得&#xff0c;如对函数和类的理解&#xff0c;对python这门工具的理解等&…...

Flask-SQLAlchemy_数据库配置

1、基本概念&#xff08;SQLAlchemy与Flask-SQLAlchemy&#xff09; SQLAlchemy 是 Python 生态中最具影响力的 ORM&#xff08;对象关系映射&#xff09;库&#xff0c;其设计理念强调 “框架无关性”&#xff0c;支持在各类 Python 项目中独立使用&#xff0c;包括 Flask、D…...

世界银行数字经济指标(1990-2022年)-社科数据

世界银行数字经济指标&#xff08;1990-2022年&#xff09;-社科数据https://download.csdn.net/download/paofuluolijiang/90623839 https://download.csdn.net/download/paofuluolijiang/90623839 此数据集涵盖了1990年至2022年间全球各国的数字经济核心指标&#xff0c;数据…...

Redis进阶知识

Redis 1.事务2. 主从复制2.1 如何启动多个Redis服务器2.2 监控主从节点的状态2.3 断开主从复制关系2.4 额外注意2.5拓扑结构2.6 复制过程2.6.1 数据同步 3.哨兵选举原理注意事项 4.集群4.1 数据分片算法4.2 故障检测 5. 缓存5.1 缓存问题 6. 分布式锁 1.事务 Redis的事务只能保…...

NY337NY340美光固态颗粒NC010NC012

NY337NY340美光固态颗粒NC010NC012 在存储技术的浩瀚星空中&#xff0c;美光的NY337、NY340、NC010、NC012等固态颗粒宛如璀璨星辰&#xff0c;闪耀着独特的光芒。它们承载着先进技术与无限潜力&#xff0c;正深刻影响着存储行业的格局与发展。 一、技术架构与核心优势 美光…...

DAY26 函数定义与参数

浙大疏锦行-CSDN博客 知识点回顾&#xff1a; 1.函数的定义 2.变量作用域&#xff1a;局部变量和全局变量 3.函数的参数类型&#xff1a;位置参数、默认参数、不定参数 4.传递参数的手段&#xff1a;关键词参数 5.传递参数的顺序&#xff1a;同时出现三种参数类型时 函数的定义…...

系统安全及应用

目录 一、账号安全控制 1.基本安全措施 &#xff08;1&#xff09;系统账号清理 (2)密码安全控制 &#xff08;3&#xff09;历史命令&#xff0c;自动注销 2.用户提权和切换命令 2.1 su命令用法 2.2 sudo命令提权 2.3通过是sudo执行特权命令 二、系统引导和登录控制…...

微信小程序 地图 使用 射线法 判断目标点是否在多边形内部(可用于判断当前位置是否在某个区域内部)

目录 射线法原理简要逻辑代码 小程序代码调试基础库小程序配置地图数据地图多边形点与多边形关系 射线法 原理 使用射线法来判断&#xff0c;目标点是否在多边形内部 这里简单说下&#xff0c;具体细节可以看这篇文章 平面几何&#xff1a;判断点是否在多边形内&#xff08;…...

第三十七节:视频处理-视频读取与处理

引言:解码视觉世界的动态密码 在数字化浪潮席卷全球的今天,视频已成为信息传递的主要载体。从短视频平台的爆火到自动驾驶的视觉感知,视频处理技术正在重塑人类与数字世界的交互方式。本指南将深入探讨视频处理的核心技术,通过Python与OpenCV的实战演示,为您揭开动态影像…...

什么是 Flink Pattern

在 Apache Flink 中&#xff0c;Pattern 是 Flink CEP&#xff08;Complex Event Processing&#xff09;模块 的核心概念之一。它用于定义你希望从数据流中检测出的 事件序列模式&#xff08;Event Sequence Pattern&#xff09;。 &#x1f3af; 一、什么是 Flink Pattern&am…...

ADB基本操作和命令

1.ADB的含义 adb 命令是 Android 官方提供&#xff0c;调试 Android 系统的工具。 adb 全称为 Android Debug Bridge&#xff08;Android 调试桥&#xff09;&#xff0c;是 Android SDK 中提供的用于管理 Android 模拟器或真机的工具。 adb 是一种功能强大的命令行工具&#x…...

NSString的三种实现方式

oc里的NSString有三种实现方式&#xff0c;为_ _NSCFConstantString、__NSCFString、NSTaggedPointerString 1._ _NSCFConstantString(字面量字符串) 从字面意思上可以看出&#xff0c;_ _NSCFContantString可以理解为常量字符串&#xff0c;这种类型的字符串在编译期就确定了…...

2025年PMP 学习二十 第13章 项目相关方管理

第13章 项目相关方管理 序号过程过程组过程组1识别相关方启动2规划相关方管理规划3管理相关方参与与执行4监控相关方参与与监控 相关方管理&#xff0c;针对于团队之外的相关方的&#xff0c;核心目标是让对方为了支持项目&#xff0c;以达到项目目标。 文章目录 第13章 项目相…...

学习黑客Kerberos深入浅出:安全王国的门票系统

Kerberos深入浅出&#xff1a;安全王国的门票系统 &#x1f3ab; 作者: 海尔辛 | 发布时间: 2025-05-18 &#x1f511; 理解Kerberos&#xff1a;为什么它如此重要&#xff1f; Kerberos是现代网络环境中最广泛使用的身份验证协议之一&#xff0c;尤其在Windows Active Dire…...

蓝桥杯19681 01背包

问题描述 有 N 件物品和一个体积为 M 的背包。第 i 个物品的体积为 vi​&#xff0c;价值为 wi​。每件物品只能使用一次。 请问可以通过什么样的方式选择物品&#xff0c;使得物品总体积不超过 M 的情况下总价值最大&#xff0c;输出这个最大价值即可。 输入格式 第一行输…...

使用 Auto-Keras 进行自动化机器学习

使用 Auto-Keras 进行自动化机器学习 了解自动化机器学习以及如何使用 auto-keras 完成它。如今&#xff0c;机器学习并不是一个非常罕见的术语&#xff0c;因为像 DataCamp、Coursera、Udacity 等组织一直在努力提高他们的效率和灵活性&#xff0c;以便将机器学习的教育带给普…...

算法刷题Day9 5.18:leetcode定长滑动窗口3道题,结束定长滑动窗口,用时1h

12. 1852.每个子数组的数字种类数 1852. 每个子数组的数字种类数 - 力扣&#xff08;LeetCode&#xff09; 思想 找到nums 所有 长度为 k 的子数组中 不同 元素的数量。 返回一个数组 ans&#xff0c;其中 ans[i] 是对于每个索引 0 < i < n - k&#xff0c;nums[i..(i …...

Protect Your Digital Privacy: Obfuscate, Don’t Hide

Protect Your Digital Privacy: Obfuscate, Don’t Hide In today’s digital world, hiding completely online is nearly impossible. But you can protect yourself by deliberately obfuscating your personal information — making it harder for others to track, pro…...

Spark 的运行模式(--master) 和 部署方式(--deploy-mode)

Spark 的 运行模式&#xff08;--master&#xff09; 和 部署方式&#xff08;--deploy-mode&#xff09;&#xff0c;两者的核心区别在于 资源调度范围 和 Driver 进程的位置。 一、核心概念对比 维度--master&#xff08;运行模式&#xff09;--deploy-mode&#xff08;部署…...

从零开始实现大语言模型(十五):并行计算与分布式机器学习

1. 前言 并行计算与分布式机器学习是一种使用多机多卡加速大规模深度神经网络训练过程&#xff0c;以减少训练时间的方法。在工业界的训练大语言模型实践中&#xff0c;通常会使用并行计算与分布式机器学习方法来减少训练大语言模型所需的钟表时间。 本文介绍PyTorch中的一种…...

生产模式下react项目报错minified react error #130的问题

这天&#xff0c;线上突然出现了一个bug&#xff0c;某个页面打开空白&#xff0c;看控制台报错minified react error #130&#xff0c;在本地看却是正常的&#xff0c;百思不得其解。 后来发现是由于线上项目它的包更新过了&#xff0c;而我本地的包没有更新&#xff0c;所以我…...

本地无损放大软件-realesrgan-gui

—————【下 载 地 址】——————— 【​本章下载一】&#xff1a;https://drive.uc.cn/s/84516041df174 【​本章下载二】&#xff1a;https://pan.xunlei.com/s/VOQDybD4ruF0-m8UJrCF-HtLA1?pwdxz9e# 【百款黑科技】&#xff1a;https://ucnygalh6wle.feishu.cn/wiki/…...

Java面试深度解析:微服务与云原生技术应用场景详解

Java面试深度解析&#xff1a;微服务与云原生技术应用场景详解 面试场景 面试官&#xff1a;我们今天的面试会围绕微服务与云原生技术展开&#xff0c;结合一个在线教育平台的业务场景进行提问。希望你放松心态&#xff0c;正常发挥。 码农明哥&#xff1a;好的好的&#xf…...

短剧小程序系统开发源码上架,短剧项目市场分析

引言 随着短视频内容消费的爆发式增长&#xff0c;短剧小程序凭借其碎片化、强互动、低成本的特点&#xff0c;成为内容创业与资本布局的新风口。2024年以来&#xff0c;行业规模突破500亿元&#xff0c;预计2027年将超千亿17。本文将深度解析短剧小程序系统开发的技术优势、市…...

常见的请求头(Request Header)参数

1. Accept 作用&#xff1a;告知服务器客户端支持的响应数据格式&#xff08;如 JSON、XML、HTML&#xff09;。示例&#xff1a;Accept: application/json&#xff08;优先接收 JSON 格式数据&#xff09;。 2. Content-Type 作用&#xff1a;说明请求体的数据格式&#xff08…...

渗透测试核心技术:内网渗透与横向移动

内网渗透是红队行动的关键阶段,攻击者通过突破边界进入内网后,需快速定位域控、横向移动并维持权限。本节从内网环境搭建、信息收集、横向移动技巧到权限维持工具,系统讲解如何在内网中隐蔽行动并扩大战果。 1. 内网环境搭建与基础配置 目标: 模拟真实企业网络,构建包含…...