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

【 Redis | 实战篇 秒杀优化 】

目录

 前言:

1.分布式锁

1.1.分布式锁的原理与方案

1.2.Redis的String结构实现分布式锁

1.3.锁误删问题

1.4.锁的原子性操作问题

1.5.Lua脚本解决原子性问题

1.6.基于String实现分布式锁存在的问题

1.7.Redisson分布式锁

2.秒杀优化

3.秒杀的异步优化

3.1.基于消息队列的异步下单思路

3.2.基于List结构的消息队列

3.3.基于PubSub的消息队列

3.4.基于Stream的消息队列

3.5.Redis作为消息队列的3种方式对比

3.6.基于Stream消息队列实现异步秒杀下单 


 前言:

解决集群模式下的安全问题(分布式锁),Redis实现秒杀优化,秒杀的异步优化

1.分布式锁

1.1.分布式锁的原理与方案

前述:由于我们发生了集群问题(不同的jvm下的监视器对象不同,那么同一把锁可以获取多次),【 Redis | 实战篇 秒杀实现 】-CSDN博客(问题描述),因此无法实现多个jvm线程的互斥

分析:其实就是因为我们使用的是jvm的锁,而多个jvm监视器并不共享,因此我们需要使用一把可以实现共享的锁(Redis的分布式锁),因为我们Redis只有一个,那么我们的资源就可以实现共享(互斥),从而避免集群问题

分布式锁介绍:满足分布式系统或集群模式下的进程可见并且互斥的锁

必须满足的要求

  • 多线程可见:所以线程都可以看见
  • 互斥:保证只有一个线程可以拿到锁,其他线程失败
  • 高可用:保证不管什么时候获取锁都会成功
  • 高性能:加锁本来就是会影响性能(串行执行),要加快获取锁的速度
  • 安全性:考虑没有成功释放锁出现的问题(死锁)

必须要求

  • 可重入性:可不可以重新来获取锁
  • 阻塞性:获取锁失败后会不会继续等待
  • 公平/非公平:获取锁是否公平

实现方案:

方案一:MySQL

MySQL:SQL型数据库

  • 多线程可见:可见,线程都可以来访问数据库
  • 互斥:互斥,线程执行操作时,我们可以向数据库来申请一个互斥锁,当事务提交时锁释放(互斥锁只允许一个线程拿到)
  • 高可用:好,利用主从机制
  • 高性能:一般,基于硬盘操作
  • 安全性:好,断开连接,自动释放锁

解释互斥:就是其实我们之前实现数据库更新数据的操作时,数据库会分配一个互斥锁,因此在更新操作时不允许多个线程来执行更新(只允许一个线程),因此我们利用这个特性,自己来从数据库申请互斥锁,实现互斥,而锁的释放数据库会通过事务的方式来进行操作(如果提交成功那么就释放),总的来说就是你只需要申请锁,锁的释放你不需要管数据库会帮你搞定

方案二:Redis

Redis:非SQL性数据库

  • 多线程可见:可见,线程直接访问
  • 互斥:互斥,利用setnx命令来实现(数据不存在才能set成功,存在则失败,因此只有一个线程能成功获取锁)
  • 高可用:好,主从,哨兵,集群机制
  • 高性能:好,基于内存操作
  • 安全性:一般,如果线程获取锁成功,服务宕机,锁没有释放(死锁),因此需要设置过期时间(时间一到自动释放锁)

解释互斥:利用Redis的命令setnx,它的原理就是看Redis中有没有对应key,没有key帮你自动创建(获取锁成功标识),有不会进行任何操作(不会覆盖)(获取锁失败标识),所以它只有第一次执行才可以真正的执行成功,那么利用它就可以实现互斥(只有一个线程才能获取成功)

解释安全性:

  • 问题:当线程获取锁成功后,还未执行释放锁操作,服务却宕机了,锁没有释放(死锁),那么以后的线程都无法获取锁,形成了死锁问题
  • 解决:既然服务宕机问题无法避免,那么我们只能从释放锁出发,因此我们可以给锁设置一个过期时间,时间一到锁自动删除(注意细节,不然还是会出问题)

方案三:Zookeeper

Zookeeper:分布式协调服务

  • 多线程可见:可见,直接访问
  • 互斥:互斥,有两种方法实现互斥,下面解释
  • 高可用:好,集群机制
  • 高性能:一般,主从之前的数据同步需要消耗一定的时间
  • 安全性:好,创建的临时节点,服务宕机自动释放

互斥方法一:利用它的节点有序性,并且节点是单调递增的,Zookeeper约定每次获取时必须获取到最小的节点才成功(保证了先执行的线程先获取小的节点,实现了线程的有序性,从而实现互斥)

互斥方法二:利用它的唯一性,由于它的节点名称都相同,那么所有线程都根据名称来获取,只有一个线程能成功获取

1.2.Redis的String结构实现分布式锁

分析:实现分布式锁那么就需要实现最基础的获取锁,释放锁

获取锁:

  • 利用setnx命令实现互斥
  • 利用expire命令设置过期时间

释放锁:

  • 手动删除锁(key)
  • 超时自动释放锁

问题:因为我们要使用的是setnx与expire两个不同的命令,分步执行,并没有确保原子性操作,那么当我们setnx执行成功,还未执行expire时,服务却宕机了,由于没有设置过期时间,如果出现之前的问题,还是会出现死锁问题(锁未释放)

解决:既然我们是因为没有确保原子性操作,那么我们就使用一个命令同时完成获取锁和设置过期实际的操作,我们可以通过使用set命令,set命令可以设置参数,而这些参数里就可以设置setnx特性(不能重复赋值)(NX),设置过期时间(EX)

思考:当我们获取锁失败后,我们应该执行什么操作?

  • 阻塞式获取:获取锁失败,会阻塞等待,等待锁释放来获取锁
  • 非阻塞式获取:获取锁失败,不继续等待,直接返回信息

实现非阻塞式获取:

步骤:

开始(跳过一些业务)

==》尝试获取锁

==》判断获取锁是否成功

==》获取锁失败

==》返回错误信息,不再重试(由于实现的是一人一单,获取锁失败则已经下过单了)

-------

==》获取锁成功,设置过期时间(原子性操作)

==》执行业务

==》业务超时/服务宕机

==》锁自动释放(删除key)

-------

==》业务执行成功

==》释放锁(删除key)

//接口
public interface ILock {boolean tryLock(Long time);void unLock();
}
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock{private  String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long time) {//1.设置keyString key = KEY_PREFIX + name;//2.存入Redis,返回//获取当前线程idString threadId = ID_PREFIX + Thread.currentThread().getId();Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, time, TimeUnit.SECONDS);return Boolean.TRUE.equals(isLock);}@Overridepublic void unLock() {//1.设置keyString key = KEY_PREFIX + name;//2.删除锁stringRedisTemplate.delete(key);}
}

解释:由于我们实现的是一人一单的业务,那么不同的用户的锁需要不同,因此我们的key需要拼接用户id,并且我们是将线程id存入value,为了区分是哪个线程执行获取锁的操作(后面要使用的细节)

1.3.锁误删问题

问题:线程1获取锁成功,由于执行业务时间过长,导致锁超时释放,而锁已经释放,线程2获取锁成功,在线程2执行业务时,线程1业务执行完直接将锁释放(删除的是线程2的锁),由于锁释放,线程3获取锁成功,执行业务,最终一人下了多次单,还是出现了并发执行的问题

前提:锁还未获取

线程1获取锁成功

==》线程1执行对应业务

==》线程1由于执行业务时发生阻塞,导致执行时间过长

==》线程1的锁自动释放

==》线程2抢到执行权

==》由于锁已经释放

==》线程2获取锁成功

==》线程2执行业务

==》线程1抢到执行权

==》线程1执行完业务

==》线程1释放锁(细节:没有判断)

==》线程3抢到执行权

==》由于锁已经释放

==》线程3获取锁成功

==》线程3执行业务

==》最终线程1,2,3都执行了业务

-----

最终我们本意是一人一单,但是现在是一人下了3次单,出现了并发执行的问题

解决:其实本质是不是因为线程1误删了线程2的锁,那么我们可不可以在每次删除锁时进行判断,先判断该锁是不是自己线程获取到的锁,如果是的那么就删除锁,不是那么就不执行删除锁操作,而我们之前是不是把线程的id存入了锁对应的value中,那么我们可以从中取出值与执行删除锁的线程id进行比较即可

 步骤:

开始(跳过一些业务)

==》尝试获取锁

==》判断获取锁是否成功

==》获取锁失败

==》返回错误信息,不再重试(由于实现的是一人一单,获取锁失败则已经下过单了

-------

==》获取锁成功,设置过期时间(原子性操作)

==》执行业务

==》业务超时/服务宕机

==》锁自动释放(删除key)

-------

==》业务执行成功

==》取出锁中存储的value(获取锁的线程id)

==》获取id与执行删除锁线程id进行判断

==》id一致

==》释放锁(删除key)

----

==》不一致,不执行删除操作

//接口
public interface ILock {boolean tryLock(Long time);void unLock();
}
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock{private  String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long time) {//1.设置keyString key = KEY_PREFIX + name;//2.存入Redis,返回//获取当前线程idString threadId = ID_PREFIX + Thread.currentThread().getId();Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, time, TimeUnit.SECONDS);return Boolean.TRUE.equals(isLock);}@Overridepublic void unLock() {//1.设置keyString key = KEY_PREFIX + name;//2.获取标识String threadId = ID_PREFIX + Thread.currentThread().getId();//3.获取Redis中的标识String id = stringRedisTemplate.opsForValue().get(key);if(threadId.equals(id)){//标识相同,释放锁//4.删除stringRedisTemplate.delete(key);}}
}

解释:由于我们之前存入了线程id,那么当我们进行删除锁操作时,我们先进行判断id是否正确,再进行删除

思考:由于线程id的创建是不断递增的,但是我们现在在集群情况下存在多个jvm它们之间的线程id不会共享,那么线程的id有可能重复,不同的jvm的线程id很可能出现重复,因此还是会出现误删问题

解决:就是说我们不仅仅是要区分不同的线程,还需要区分不同的tomcat(jvm),总的来说就是需要区分不同jvm下的不同线程,所以我们在存入vaule值的时候就需要存入区分jvm的标识

实现:我们生成一个全局变量UUID(static,final),用UUID区分不同的jvm,用线程id区分不同的线程,将生成的UUID拼接上线程id存入value即可

1.4.锁的原子性操作问题

问题:当线程1获取锁成功后执行业务,在判断锁是否一致,锁一致,但是此时发生了阻塞(jvm的垃圾回收可能会阻塞),由于阻塞时间过长,发生锁的超时释放,由于锁释放了,线程2获取锁成功后,线程1来执行删除锁的操作(已经判断过一致了),其实是把线程2的锁删除,线程3获取锁,执行业务,最终还是出现了多个线程执行业务,出现并发执行问题

前提:此时锁还未获取

线程1获取锁成功

==》线程1执行业务

==》线程1判断锁是否一致

==》线程1判断成功

==》线程1还未删除锁发生了阻塞(jvm有垃圾回收机制可能会操作阻塞)

==》线程1由于阻塞时间过长,导致锁超时释放

==》由于锁已经释放

==》线程2抢到执行权

==》线程2获取锁成功

==》线程2执行业务

==》线程2判断锁是否一致

==》线程2判断成功

==》线程1抢到执行权

==》由于之前已经进行了判断操作,可以直接删除

==》线程1执行删除锁操作(删除线程2的锁)

==》线程3抢到执行权

==》线程3获取锁成功

==》线程3执行业务

==》线程2也会删除线程3的锁

==》循环执行

------

最终一个用户可以下多个单,出现了并发执行问题

 原因:其实出现问题的原因还是因为判断锁标识和释放锁标识是两个动作(如果之间发生阻塞,那么就会出现问题),因此我们还是需要进行原子性操作

思考:一般想到的解决方案是不是进行事务管理,同时成功才事务提交,失败一个就事务回滚

Redis的事务:Redis的事务是一个批处理操作只会一次性就全部执行完,并不会有分布操作),因为你的判断操作是需要查询数据来进行判断,如果你将判断锁和删除锁加入Redis事务,那么你的查询数据的结果需要等到删除锁操作执行时才会有数据(一次性全部执行),因此此方法行不通

1.5.Lua脚本解决原子性问题

Redis来执行Lua脚本:Redis提供了Lua脚本功能,在一个脚本中可以编写多条Redis命令,确保命令执行时的原子性,而Lua是一个编程语言

语法:redis.call('命令名称','key','其他参数',........)(脚本)

在Redis中调用Lua脚本:EVAL "脚本"  0(代表脚本要使用的key个数)(如果你是在命令中写死了key,那么个数就写0,没有写死,而是写的KEYS[N],那么个数就写N)

比如:EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name  Rose

解释:就是说你这里写了key的个数为1,那么它就会从开头找一位参数(name),找完了那么剩余的就是其他参数,Rose就是ARGV[1]

RedisTemplate调用Lua脚本:

-- 比较线程标识是否与锁中的标识一致
if (redis.call('get',KEYS[1]) == ARGV[1]) then-- 释放锁return redis.call('del',KEYS[1])
end
return 0
public class SimpleRedisLock implements ILock{private  String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<Long>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long time) {//1.设置keyString key = KEY_PREFIX + name;//2.存入Redis,返回//获取当前线程idString threadId = ID_PREFIX + Thread.currentThread().getId();Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, time, TimeUnit.SECONDS);return Boolean.TRUE.equals(isLock);}@Overridepublic void unLock() {//调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());}}

1.6.基于String实现分布式锁存在的问题

问题:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 例子:在同一个线程中方法a获取锁后调用方法b,b也要获取锁,b获取锁失败,如果你是可重试机制,b就会一直等待a将锁释放,而a需要调用b执行完才能释放,从而出现死锁问题
  • ----------
  • 不可重试:实现了非阻塞式,尝试一次获取,失败就返回错误信息
  • 注意:由于实现了不可重试机制,其实上面的例子只能用来理解一下
  • ----------
  • 超时释放:虽然可以避免死锁问题,但是会出现超时误删锁的问题,存在安全隐患
  • ----------
  • 主从一致性:主从同步存在延迟,当主宕机时,就会出现问题
  • 原理:主节点负责写操作,从节点负责读操作,读是从多个节点读,并且当主出现问题时,从会代主
  • 例子:线程1获取锁(set写操作),主节点完成(同步延迟),还未同步到从节点,主节点宕机,从代主(未同步锁),线程2就可以获取锁
  • 解释:虽然有这种情况,但是由于主从延迟可以做到毫秒及一下,所以其概率极低

解决:简单说一下

不可重入是因为你锁只有一次使用权,那么我们可以给锁加个次数,先判断是不是同一个jvm下的同一个线程,是的那就给锁的次数加一,当每次删除锁时先进行判断是不是自己的锁,然后进行次数减一,最后判断次数是不是已经为0,为0才可以删除锁,细节:由于现在有三个字段(key,value,次数)因此我们要使用Hash结构来实现

不可重试:就是更改一段业务代码,既然你需要重试,那么就重试(细节:不要获取锁失败就之间重试,可以等一等,利用订阅和信号量来解决)

超时释放:其实就是因为我们执行业务时,由于业务时间过长导致释放,那么我们可以进行一个判断,在超时时间的三分之一处(别处也可以)你的业务还在执行,那么我就刷新你的锁超时时间,你一直在执行,那么我就一直刷新(细节:利用watchDog)

主从一致性:既然是因为主从同步出现问题,那就不要主从了,直接让所有节点变成Redis的独立节点(都可以进行读写操作),以前获取锁只需要访问主节点,现在你需要访问所有的独立节点,都同意你才能获取到锁(都存入了锁数据)

1.7.Redisson分布式锁

介绍:在Redis的基础上实现了一个分布式工具集合(类似工具包),就是说你不需要自己来实现分布式锁了,直接用它就行

实现步骤:

  • 引入依赖:
  • 配置Redisson客户端:

 使用:直接调用方法,给参数就行(和之前我们自己定义的差不多)

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;//代理对象IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) throws InterruptedException {//1.根据id查询数据库优惠券信息SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.获取时间LocalDateTime beginTime = voucher.getBeginTime();LocalDateTime endTime = voucher.getEndTime();//3.判断时间if (beginTime.isAfter(LocalDateTime.now())) {return Result.fail("秒杀还未开始");}if (endTime.isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}//4.获取库存Integer stock = voucher.getStock();//库存不足if (stock < 1) {return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);if (!isLock) {//获取锁失败return Result.fail("只能下一单");}//获取锁成功//获取代理对象try {IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.creatOrder(voucherId);} catch (IllegalStateException e) {throw new RuntimeException(e);} finally {lock.unlock();}}@Transactionalpublic Result creatOrder(Long voucherId) {//根据用户id和优惠券id查询数据库Long userId = UserHolder.getUser().getId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {//该用户已经下过单了return Result.fail("一个用户只能下一单");}//库存足//5.库存减1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0)//乐观锁.update();if (!success) {return Result.fail("库存不足");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();//优惠券idvoucherOrder.setVoucherId(voucherId);//订单idLong orderId = redisIdWorker.setId("order");voucherOrder.setId(orderId);//用户idvoucherOrder.setUserId(userId);//存入数据库save(voucherOrder);return Result.ok(orderId);}
}

2.秒杀优化

分析:由于我们进行的查询更新操作都是直接对数据库进行操作,而数据库的并发能力本身是比较差的(写操作更慢),并且为了安全问题,我们还加入了分布式锁(影响性能),假设同时有大量的用户来访问(串行执行),一个接一个(等待时间过长)

例子:在一家成本有限的饭店里,店主既要当服务员又要当厨师,当一名顾客来点单,店主需要接待顾客,然后进行炒菜(这个时间就长了),如果有下一个顾客也来点单,但是由于店主正在炒菜,顾客需要等待,你说他会不会等待这么久(工作效率低)

例子解决:是不是因为店主需要干的事情太多了,那么店主就必须多聘用几人,分别负责不同的工作,这样效率就提高了

问题解决:因此我们也需要将业务操作分别由不同的线程来执行,效率就提高了

思考:怎么分开呢?

例子:在饭店里,当用户点餐后,服务员是不是需要给用户一个小票记录用户的单号,而厨师那里也需要一个小票,他需要根据单号来依次炒菜,这样是不是实现了异步执行,服务员只需要等待用户下完单后给小票,然后他就可以接待下一个顾客了(无需过度等待),而用户只需要等餐就行,工作效率大大提高(给完小票就是代表下单成功,之前是给餐后才是下单成功)

再次解决:因此我们也可以实现该思想,我们可以将查询,判断库存,校验一人一单的操作类比于服务员接单操作,而我们的创建订单操作类比于厨师炒菜操作(时间长的你就可以类比厨师炒菜),我们判断校验成功后直接给用户返回下单成功,而具体的创建下单操作用户无需等待,类比后台执行(它会帮我们执行完,异步执行)

总结思路下单操作是不是只需要是判断校验成功,那么他就可以下单,我们就是在判断校验成功直接返回下单成功信息(而下单操作异步执行),这样就大大增加了效率

优化:既然是先查询判断校验,然后异步更新数据库,那么我们可不可以将查询数据库转变成查询Redis(效率再次提高)

实现思路:我们将需要用到的查询数据存入Redis,判断校验成功后,将具体订单信息存入阻塞队列中,然后直接返回订单id即可,异步(新的线程)从队列中取出数据,执行创建订单操作(更新数据库)

思考如何将数据存入Redis:判断时间操作不需要我们判断了,其实前端就已经进行了判断,符合要求的你才能下单,判断库存呢,使用Redis的String结构(key为优惠券id,value为库存数量),校验一人一单呢?我们是不是可以这样思考:我们使用set集合(不可重复特性),key为优惠券id,value为用户id,因为value不可重复,因此只能存在不同的用户id,用户下单时,如果查到这个优惠券已经有该用户时,校验不通过,反之通过

细节:由于我们Redis同步了数据库的库存,那么其实当校验通过时,我们的Redis是不是也需要扣减库存,并且在操作Redis时,我们是不是也需要保证原子性操作(使用Lua脚本)

步骤:

Lua脚本

开始(操作Redis)

==》判断库存是否充足

==》库存不足

==》返回1(约定标识)

-------

==》库存充足

==》判断用户是否下过单

==》用户已经下过单

==》返回2(约定标识)

-------

==》用户没有下过单

==》扣减库存(-1)

==》将用户id存入当前优惠券的set集合

==》返回0(约定标识)


--获取id
local voucherId = ARGV[1]
local userId = ARGV[2]--获取key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId--获取库存,判断
local stock = tonumber(redis.call('get', stockKey))
if not stock or stock <= 0 thenreturn 1 -- 库存不足
end
--判断用户是否重复下单
if(redis.call('sismember',orderKey,userId) == 1) then--已经下过单return 2
end
-- 扣减库存
redis.call('incrby',stockKey,-1)
-- 保存用户id到Redis中
redis.call('sadd',orderKey,userId)-- 返回
return 0

服务端

前端传过来优惠券id

==》后端接收id

==》传入Lua脚本需要的用户id和优惠券id

==》执行Lua脚本

==》判断返回结果是否为0

==》结果不为0

==》根据返回结果,返回对应的错误信息

==》1(库存不足),2(不能重复下单)

----

==》结果为0

==》将优惠券id和用户id和订单id存入阻塞队列

==》调用新的线程异步执行更新数据库操作(下单操作)

==》直接返回订单id(下单成功信息)

 

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<Long>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}//队列private final BlockingQueue<VoucherOrder>  orderTasks = new ArrayBlockingQueue<>(1024*1024);//线程池private static final ExecutorService  SCRIPT_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//代理对象IVoucherOrderService proxy;@PostConstructprivate void init(){//类加载就执行方法SCRIPT_ORDER_EXECUTOR.submit(new OrderRunTask());}private class OrderRunTask implements Runnable{@Overridepublic void run() {while (true) {try {//1.获取队列信息VoucherOrder voucherOrder = orderTasks.take();//2.创建订单handleOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常:",e);}}}}private void handleOrder(VoucherOrder voucherOrder) throws InterruptedException {//设置锁Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);if (!isLock) {//获取锁失败log.error("获取锁失败");return ;}//获取锁成功//获取代理对象try {//创建订单proxy.creatOrderTask(voucherOrder);} catch (IllegalStateException e) {throw new RuntimeException(e);} finally {lock.unlock();}}@Overridepublic Result seckillVoucher(Long voucherId) {//1.获取用户idLong userId = UserHolder.getUser().getId();//2.执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());//3.判断long r = result.longValue();if (r != 0){//执行失败,无法下单return Result.fail(r==1?"库存不足":"无法重复下单");}//4.成功执行,可以下单// 阻塞队列Long orderId = redisIdWorker.setId("order");//5.创建订单VoucherOrder voucherOrder = new VoucherOrder();//5.1优惠券idvoucherOrder.setVoucherId(voucherId);//5.2订单idvoucherOrder.setId(orderId);//5.3用户idvoucherOrder.setUserId(userId);//6.加入阻塞队列orderTasks.add(voucherOrder);proxy = (IVoucherOrderService) AopContext.currentProxy();return Result.ok(orderId);}@Transactionalpublic void creatOrderTask(VoucherOrder voucherOrder) {//根据用户id和优惠券id查询数据库Long userId = voucherOrder.getUserId();Long voucherId = voucherOrder.getVoucherId();int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {//该用户已经下过单了log.error("不能重复下单");return;}//库存足//5.库存减1boolean success = iSeckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock", 0)//乐观锁.update();if (!success) {return;}//存入数据库save(voucherOrder);}
}

解释:

阻塞队列:有一个线程尝试去队列中获取元素时,有元素获取成功,没有那么该线程就会被阻塞(一直等待),直到队列中有元素,获取到元素,才能继续执行后续操作

线程池:准备线程池,来实现新线程异步执行下单操作,准备一个线程任务(下单)让线程执行(一直)

  • 思考:是不是当我们的项目一启动,用户就可以进行下单了,因此我们需要在类加载完毕时就开始执行任务,使用注解@PostConstruct
  • 再思考:由于我们是开启一个新的线程来创建订单,而不同的线程的TreadLocal空间并不共享,所以无法从中获取,同理代理对象也不能获取(原理也是根据线程id来获取的,而我们的线程id已经变化)

解决:我们本来就已经将用户id存入了阻塞队列,我们直接从队列中取值就行,而代理对象也可以将其存入阻塞队列中或者是定义一个成员变量(先在主线程将变量赋值,新的线程直接调用即可)

内存限制问题:由于我们使用的阻塞队列基于jvm来实现,使用的是jvm的内存,如果同时有大量用户下单(队列中的任务还没有来得及执行,内存没来得及释放),从而导致队列中的内存用完了,那么在之后下单的用户不会下单成功(内存限制)

数据安全问题:

  • 原理是先在Redis中保证订单信息,再由新的线程操作数据库完成下单,那如果在操作数据库之前(下单之前),服务器宕机了,没有下单成功(数据库中没有订单数据),Redis和数据库中的数据不一致
  • 原理线程从队列中取出任务后,该任务在队列中就已经删除了,那如果线程取出任务执行时发生了事故,导致任务还未执行完就终止了,而此时队列中也没有该任务了(数据库也没有进行下单操作),数据还是会不一致

3.秒杀的异步优化

3.1.基于消息队列的异步下单思路

分析:我们出现了问题是内存泄漏和数据安全问题,内存泄漏好解决,那么换一个,数据安全呢

例子:在之前,还没有快递柜时,快递员送快递需要受限于用户是否有时间接收(用户在上班上面的),如果用户很忙,快递员先把快递放门口,而用户担心快递被偷,不放门口一直等用户也不行,用户请假回家拿快递也不行,这样造成了双方不好的局面

解决:如果我们设置一个快递柜,快递员只需要将快递送到快递柜即可,用户有时间了自己去拿一下就行,而快递柜既保证了快递的安全也保证了快递的存放数量

思考:我们是不是也可以这样,生产者为快递员,队列为快递柜,消费者为用户,那我们该使用什么队列呢?使用消息队列

消息队列的介绍:存放消息的队列,最简单的消息队列包含3个角色

  • 消息队列:存储和管理消息
  • 生成者:发送消息到消息队列中
  • 消费者:从消息队列中获取消息并处理消息

优势:

  • 它独立于jvm,不受jvm内存限制
  • 不仅仅可以做消息的存储还可以做持久化(消息确认机制:你取出消息后,你需要消息确认,没有确认队列中的消息就不会消失,确保消息至少被消费一次)

基于Redis来实现消息队列的方式:

  • List结构:基于List结构来模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型

3.2.基于List结构的消息队列

队列的基于原理:先进的先出去,出口与入口不一致

分析:那么我们就可以使用对应的List命令来实现(左边存,右边取,或者反之)

思路:我们具体使用的命令:BRPOP(左边存,右边取),这个命令它可以设置等待时间,那就代表使用该命令可以实现阻塞式获取数据

优点:

  • 使用Redis,不受jvm内存限制
  • List本身是Redis的数据结构,因此支持持久化,保证数据安全
  • 满足有序性

缺点:

  • 无法避免消息丢失:没有消息确认机制,消费者取出消息后,List中消息删除,而如果消费者自己出现了问题没有消费,导致消息丢失
  • 只支持单消费者:拿了消息就会删除消息(只能使用一次)

3.3.基于PubSub的消息队列

介绍:消息传递模型(广播),消费者可以订阅一个或多个channel(类似频道),只要生产者向对应频道发送消息,那么所有订阅该频道的消费者就都可以收到消息

优点:

  • 使用Redis,不受jvm内存限制
  • 采用发布订阅支持多消费多生成
  • 满足有序性

缺点:

  • 不支持数据持久化:发送一条消息,没人订阅,那么消息就会消失,并不会将消息保证到Redis中
  • 无法避免消息丢失:发送消息没人接收,那就丢失了
  • 消息堆积有上限,超出数据丢失:发送消息时,如果有消费者订阅(监听),那么消费者那里会有一个缓存区域(临时存储消息),消费完一条消息,缓存就减一条消息,如果突然有大量消息发出,消费者来不及处理,而缓存空间有限,超出空间数据丢失

3.4.基于Stream的消息队列

基础命令:

命令XREAD特点:

  • 消息可回溯
  • 可以多消费者抢消息(竞争),加快消费速度
  • 可以阻塞读取
  • 没有消息漏读风险
  • 有消息确认机制,保证消息至少被消费一次

消费者组:

3.5.Redis作为消息队列的3种方式对比

1. 消息持久化

  • List
    支持持久化,消息存储在内存中,可通过RDB/AOF机制持久化到磁盘。适合需要简单持久化的场景,但需注意内存容量限制。

  • PubSub
    不支持持久化。消息仅在发布时推送给当前在线的订阅者,若订阅者离线则消息丢失。适用于实时通知等临时性场景。

  • Stream
    支持持久化,消息按时间顺序存储,可长期保留。支持数据备份和恢复,适合需要高可靠性的场景。

2. 阻塞读取

  • List
    支持阻塞读取(如BLPOP命令),消费者可等待新消息到达,避免轮询资源浪费。适合需要长连接等待消息的场景。

  • PubSub
    不支持阻塞读取。订阅者需在线才能接收消息,消息即时推送后即失效,无法主动拉取历史消息。

  • Stream
    支持阻塞读取(如XREAD命令),消费者可阻塞等待新消息,并支持指定超时时间。结合消费者组时,能实现高效的消息分发。

3. 消息堆积处理

  • List
    消息堆积受限于内存空间,需通过多消费者并行消费(如多个客户端轮询同一List)加快处理速度。适用于低吞吐量场景,但需警惕内存溢出风险。

  • PubSub
    消息堆积能力极弱,受限于消费者缓冲区。若消费者处理速度慢,可能导致消息丢失或缓冲区溢出。仅适合瞬时流量场景。

  • Stream
    支持设定队列最大长度(MAXLEN),超过时自动淘汰旧消息。通过消费者组(Consumer Group)实现负载均衡,多个消费者可并行处理同一队列,显著减少堆积风险。适合高并发场景。

4. 消息确认机制

  • List
    不支持消息确认。消息一旦被消费者读取即从队列移除,若消费失败无法重新投递。需自行实现重试逻辑。

  • PubSub
    不支持消息确认。消息推送后即丢弃,无重试机制,可靠性较低。

  • Stream
    支持消息确认(XACK)。消费者处理消息后需显式确认,若未确认,消息会重新分配给其他消费者。结合消费者组的Pending Entries List(PEL),可实现可靠的消息投递。

5. 消息回溯

  • List
    不支持消息回溯。消息被消费后即从队列头部移除,无法重新访问历史数据。

  • PubSub
    不支持消息回溯。消息发布后仅推送给当前订阅者,无法追溯历史记录。

  • Stream
    支持消息回溯。通过消息ID(时间戳+序号)可精确读取历史消息(如XREAD指定起始ID),便于故障恢复或数据重放。

3.6.基于Stream消息队列实现异步秒杀下单 

Lua脚本:


--获取id
local voucherId = ARGV[1]
local userId = ARGV[2]--orderId
local orderId = ARGV[3]--获取key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId--获取库存,判断
local stock = tonumber(redis.call('get', stockKey))
if not stock or stock <= 0 thenreturn 1 -- 库存不足
end
--判断用户是否重复下单
if(redis.call('sismember',orderKey,userId) == 1) then--已经下过单return 2
end
-- 扣减库存
redis.call('incrby',stockKey,-1)
-- 保存用户id到Redis中
redis.call('sadd',orderKey,userId)-- 发送消息
redis.call('xadd','stream.orders','*','userId',userId,'voucherId',voucherId,'id',orderId)
-- 返回
return 0
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<Long>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}//线程池private static final ExecutorService  SCRIPT_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//代理对象IVoucherOrderService proxy;@PostConstructprivate void init(){//类加载就执行方法SCRIPT_ORDER_EXECUTOR.submit(new OrderRunTask());}private class OrderRunTask implements Runnable{String queueName = "stream.orders";@Overridepublic void run() {while (true) {try {//1.获取队列信息List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lastConsumed()));//2.判断消息是否获取成功if(list == null || list.isEmpty()){//2.1.获取失败,没有消息,继续循环continue;}//解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> values = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);//3.获取成功,可以下单handleOrder(voucherOrder);//4.ACK确认stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());} catch (Exception e) {log.error("处理订单异常:",e);handlePendingList();}}}private void handlePendingList() {while (true) {try {//1.获取队列信息List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from("0")));//2.判断消息是否获取成功if(list == null || list.isEmpty()){//2.1.获取失败,说明pendList没有异常消息,退出break;}//解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> values = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);//3.获取成功,可以下单handleOrder(voucherOrder);//4.ACK确认stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());} catch (Exception e) {log.error("处理pending-list订单异常:",e);try {Thread.sleep(20);} catch (InterruptedException ex) {throw new RuntimeException(ex);}}}}}@Overridepublic Result seckillVoucher(Long voucherId) {//1.获取用户idLong userId = UserHolder.getUser().getId();//订单idLong orderId = redisIdWorker.setId("order");//2.执行lua脚本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(),orderId.toString());//3.判断long r = result.longValue();if (r != 0){//执行失败,无法下单return Result.fail(r==1?"库存不足":"无法重复下单");}//获取代理对象proxy = (IVoucherOrderService) AopContext.currentProxy();//返回订单idreturn Result.ok(orderId);}}

问题:

  • Redis持久化数据还是会出现数据丢失风险
  • 只支持消费者确认,不支持生产者(如果是生产者发送消息时,出现了消息丢失呢?)

解决:使用专门的消息中间件

相关文章:

【 Redis | 实战篇 秒杀优化 】

目录 前言&#xff1a; 1.分布式锁 1.1.分布式锁的原理与方案 1.2.Redis的String结构实现分布式锁 1.3.锁误删问题 1.4.锁的原子性操作问题 1.5.Lua脚本解决原子性问题 1.6.基于String实现分布式锁存在的问题 1.7.Redisson分布式锁 2.秒杀优化 3.秒杀的异步优化 3.1…...

【Spring】核心机制:IOC与DI深度解析

目录 1.前言 2.正文 2.1三层架构 2.2Spring核心思想&#xff08;IOC与AOP&#xff09; 2.3两类注解&#xff1a;组件标识与配置 2.3.1五大类注解 2.3.1.1Controller 2.3.1.2Service 2.3.1.3Repository 2.3.1.4Configuration 2.3.1.5Component 2.3.2方法注解&#x…...

1-机器学习的基本概念

文章目录 一、机器学习的步骤Step1 - Function with unknownStep2 - Define Loss from Training DataStep3 - Optimization 二、机器学习的改进Q1 - 线性模型有一些缺点Q2 - 重新诠释机器学习的三步Q3 - 机器学习的扩展Q4 - 过拟合问题&#xff08;Overfitting&#xff09; 一、…...

ARM A64 STR指令

ARM A64 STR指令 1 STR (immediate)1.1 Post-index1.1.1 32-bit variant1.1.2 64-bit variant 1.2 Pre-index1.2.1 32-bit variant1.2.2 64-bit variant 1.3 Unsigned offset1.3.1 32-bit variant1.3.2 64-bit variant 1.4 Assembler symbols 2 STR (register)2.1 32-bit varia…...

虚幻引擎5-Unreal Engine笔记之`GameMode`、`关卡(Level)` 和 `关卡蓝图(Level Blueprint)`的关系

虚幻引擎5-Unreal Engine笔记之GameMode、关卡&#xff08;Level&#xff09; 和 关卡蓝图&#xff08;Level Blueprint&#xff09;的关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之GameMode、关卡&#xff08;Level&#xff09; 和 关卡蓝图&#xff08;Level B…...

软件工具:批量图片区域识别+重命名文件的方法,发票识别和区域选择方法参考,基于阿里云实现

基于阿里云的批量图片区域识别与重命名解决方案 图像识别重命名 应用场景 ​​企业档案管理​​&#xff1a;批量处理扫描的合同、文件等图片&#xff0c;根据合同编号、文件标题等关键信息重命名文件​​医疗影像处理​​&#xff1a;识别X光、CT等医学影像中的患者ID、检查日…...

.NET外挂系列:1. harmony 基本原理和骨架分析

一&#xff1a;背景 1. 讲故事 为什么要开这么一个系列&#xff0c;是因为他可以对 .NET SDK 中的方法进行外挂&#xff0c;这种技术对解决程序的一些疑难杂症特别有用&#xff0c;在.NET高级调试 领域下大显神威&#xff0c;在我的训练营里也是花了一些篇幅来说这个&#xf…...

深入理解位图(Bit - set):概念、实现与应用

目录 引言 一、位图概念 &#xff08;一&#xff09;基本原理 &#xff08;二&#xff09;适用场景 二、位图的实现&#xff08;C 代码示例&#xff09; 三、位图应用 1. 快速查找某个数据是否在一个集合中 2. 排序 去重 3. 求两个集合的交集、并集等 4. 操作系…...

React Flow 边事件处理实战:鼠标事件、键盘操作及连接规则设置(附完整代码)

本文为《React Agent&#xff1a;从零开始构建 AI 智能体》专栏系列文章。 专栏地址&#xff1a;https://blog.csdn.net/suiyingy/category_12933485.html。项目地址&#xff1a;https://gitee.com/fgai/react-agent&#xff08;含完整代码示​例与实战源&#xff09;。完整介绍…...

【计算机网络】第一章:计算机网络体系结构

本篇笔记课程来源&#xff1a;王道计算机考研 计算机网络 【计算机网络】第一章&#xff1a;计算机网络体系结构 一、计算机网络的概念1. 理论2. 计算机网络、互连网、互联网的区别 二、计算机网络的组成、功能1. 组成2. 功能 三、交换技术1. 电路交换2. 报文交换3. 分组交换4.…...

实战设计模式之状态模式

概述 作为一种行为设计模式&#xff0c;状态模式允许对象在其内部状态改变时&#xff0c;改变其行为。这种模式通过将状态逻辑从对象中分离出来&#xff0c;并封装到独立的状态类中来实现。每个状态类代表一种特定的状态&#xff0c;拥有自己的一套行为方法。当对象的状态发生变…...

[C++入门]类和对象中(2)日期计算器的实现

目录 一、运算符重载 1、格式 2、简单举例 2、前置&#xff0c;后置 3、日期生成器的实现 1、声明与定义 1、友元函数 2、print函数 3、运算符重载 4、GetMonthDay 5、&#xff0c;-&#xff0c;&#xff0c;-的实现 6、重载流操作符 2、实现 3、定义源码 一、运算…...

数据质量问题的形成与解决

在数字化时代&#xff0c;数据已成为企业和组织发展的核心资产&#xff0c;数据质量的高低直接影响着决策的准确性、业务的高效性以及系统的稳定性。然而&#xff0c;数据质量问题频发&#xff0c;严重阻碍了数据价值的充分发挥。 一、数据质量问题的成因分析 1.信息因素 元数…...

论文阅读(四):Agglomerative Transformer for Human-Object Interaction Detection

论文来源&#xff1a;ICCV&#xff08;2023&#xff09; 项目地址&#xff1a;https://github.com/six6607/AGER.git 1.研究背景 人机交互&#xff08;HOI&#xff09;检测需要同时定位人与物体对并识别其交互关系&#xff0c;核心挑战在于区分相似交互的细微视觉差异&#…...

【机器学习】工具入门:飞牛启动Dify Ollama Deepseek

很久没有更新文章了,最近正好需要研究一些机器学习的东西&#xff0c;打算研究一下 difyOllama 以下是基于FN 的dify本地化部署&#xff0c;当然这也可能是全网唯一的飞牛部署dify手册 部署 官方手册&#xff1a;https://docs.dify.ai/en/getting-started/install-self-hos…...

课外活动:再次理解页面实例化PO对象的魔法方法__getattr__

课外活动&#xff1a;再次理解页面实例化PO对象的魔法方法__getattr__ 一、动态属性访问机制解析 1.1 核心实现原理 class Page:def __getattr__(self, loc):"""魔法方法拦截未定义属性访问"""if loc not in self.locators.keys():raise Exce…...

面试题总结二

1.mybatis三个范式 第一范式&#xff1a;表中字段不能再分&#xff0c;每行数据都是唯一的第二范式&#xff1a;满足第一范式&#xff0c;非主键字段只依赖于主键第三范式&#xff1a;满足第二范式&#xff0c;非主键字段没有传递依赖 2.MySQL数据库引擎有哪些 InnoDB&#…...

代码随想录算法训练营第六十六天| 图论11—卡码网97. 小明逛公园,127. 骑士的攻击

继续补&#xff0c;又是两个新算法&#xff0c;继续进行勉强理解&#xff0c;也是训练营最后一天了&#xff0c;六十多天的刷题告一段落了&#xff01; 97. 小明逛公园 97. 小明逛公园 感觉还是有点难理解原理 Floyd 算法对边的权值正负没有要求&#xff0c;都可以处理。核心…...

编程技能:字符串函数07,strncat

专栏导航 本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏&#xff0c;故划分为两个专栏导航。读者可以自行选择前往哪个专栏。 &#xff08;一&#xff09;WIn32 专栏导航 上一篇&#xff1a;编程技能&#xff1a;字符串函数06&#xff0c;strcat 回到目录…...

[Java实战]Spring Boot整合RabbitMQ:实现异步通信与消息确认机制(二十七)

[Java实战]Spring Boot整合RabbitMQ&#xff1a;实现异步通信与消息确认机制&#xff08;二十七&#xff09; 摘要&#xff1a;本文通过完整案例演示Spring Boot与RabbitMQ的整合过程&#xff0c;深入讲解异步通信原理与消息可靠性保证机制。包含交换机类型选择、消息持久化配…...

数据库中关于查询选课问题的解法

前言 今天上午起来复习了老师上课讲的选课问题。我总结了三个解法以及一点注意事项。 选课问题介绍 简单来说就是查询某某同学没有选或者选了什么课。然后查询出该同学的姓名&#xff0c;学号&#xff0c;课程号&#xff0c;课程名之类的。 sql文件我上传了。大家可以尝试练…...

用 UniApp 开发 TilePuzzle:一个由 CodeBuddy 主动驱动的拼图小游戏

我正在参加CodeBuddy「首席试玩官」内容创作大赛&#xff0c;本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 起心动念&#xff1a;从一个小游戏想法开始 最近在使用 UniApp 做练手项目的时候&#xff0c;我萌生了一个小小…...

golang 安装gin包、创建路由基本总结

文章目录 一、安装gin包和热加载包二、路由简单场景总结 一、安装gin包和热加载包 首先终端新建一个main.go然后go mod init ‘项目名称’执行以下命令 安装gin包 go get -u github.com/gin-gonic/gin终端安装热加载包 go get github.com/pilu/fresh终端输入fresh 运行 &…...

组态王|组态王中如何添加西门子1200设备

哈喽,你好啊,我是雷工! 最近使用组态王采集设备数据,设备的控制器为西门子的1214CPU, 这里边实施边记录,以下为在组态王中添加西门子1200PLC的笔记。 1、新建 在组态王工程浏览器中选择【设备】→点击【新建】。 2、选择设备 和设备建立通讯要通过对应的设备驱动。 在…...

碎片笔记|PromptStealer复现要点(附Docker简单实用教程)

前言&#xff1a;本篇博客记录PromptStealer复现历程&#xff0c;主要分享环境配置过程中的一些经验。 论文信息&#xff1a;Prompt Stealing Attacks Against Text-to-Image Generation Models. USENIX, 2024. 开源代码&#xff1a;https://github.com/verazuo/prompt-stealin…...

Docker配置SRS服务器 ,ffmpeg使用rtmp协议推流+vlc拉流

目录 演示视频 前期配置 Docker配置 ffmpeg配置 vlc配置 下载并运行 SRS 服务 推拉流流程实现 演示视频 2025-05-18 21-48-01 前期配置 Docker配置 运行 SRS 建议使用 Docker 配置 Docker 请移步&#xff1a; 一篇就够&#xff01;Windows上Docker Desktop安装 汉化完整指…...

c++学习之--- list

目录 ​编辑 一、list的定义: 二、list的模拟实现&#xff1a; 1、list的基本框架&#xff1a; 2、list的普通迭代器&#xff1a; 设计思想&#xff1a; 迭代器的一个特殊需求&#xff08;c 对于重载->的一颗语法糖&#xff09;&#xff1a; 代码实现&#xff1a; 3、cons…...

【C++】set、map 容器的使用

文章目录 1. set 和 multiset 的使用1.1 set类的介绍1.2 set的构造和迭代器1.3 set 的增删查1.4 insert和迭代器调用示例1.5 find和erase使用示例1.6 multiset和set的差异 2. map 和 multimap 的使用2.1 map 类的介绍2.2 pair 类型介绍2.3 map 的构造和迭代器2.4 map 的增删查2…...

实习记录小程序|基于SSM+Vue的实习记录小程序设计与实现(源码+数据库+文档)

实习记录小程序 目录 基于SSM的习记录小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 1、小程序端&#xff1a; 2、后台 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码…...

Git从入门到精通

Git 是什么 Git 是一个分布式版本控制系统&#xff0c;主要用于跟踪和管理文件&#xff08;尤其是代码&#xff09;的变更。 Git的下载与安装 进入git官网下载界面,选择Windows系统。 点击选择Git for Windows/x64 Setup,进行安装。 注意: Git GUI 是Git提供的一个图形界面工…...

Binary Prediction with a Rainfall Dataset-(回归+特征工程+xgb)

Binary Prediction with a Rainfall Dataset 题意&#xff1a; 给你每天的天气信息&#xff0c;让你预测降雨量。 数据处理&#xff1a; 1.根据特征值构造天气降雨量的新特征值 2.根据时间构造月和季节特征 3.处理缺失值 建立模型&#xff1a; 1.建立lightgbm模型 2.建立…...

【C++】unordered_map与set的模拟实现

unordered系列map和set&#xff0c;与普通区别 用法几乎相同&#xff0c;键值唯一&#xff0c;区别unordered系列迭代器是单向的并且遍历出来不是有序的。unordered系列在数据规模大且无序的情况下性能更优 底层实现&#xff1a; map 和 set &#xff1a;基于平衡二叉树&…...

老旧设备升级利器:Modbus TCP转 Profinet让能效监控更智能

在工业自动化领域&#xff0c;ModbusTCP和Profinet是两种常见的通讯协议。Profinet是西门子公司推出的基于以太网的实时工业以太网标准&#xff0c;而Modbus则是由施耐德电气提出的全球首个真正开放的、应用于电子控制器上的现场总线协议。这两种协议各有各的优点&#xff0c;但…...

编译原理--期末复习

本文是我学习以下博主视频所作的笔记&#xff0c;写的不够清晰&#xff0c;建议大家直接去看这些博主的视频&#xff0c;他/她们讲得非常好&#xff1a; 基础知识概念&#xff1a; 1.【【编译原理】期末复习 零基础自学】&#xff0c;资料 2.【编译原理—混子速成期末保过】&…...

软件工程各种图总结

目录 1.数据流图 2.N-S盒图 3.程序流程图 4.UML图 UML用例图 UML状态图 UML时序图 5.E-R图 首先要先了解整个软件生命周期&#xff1a; 通常包含以下五个阶段&#xff1a;需求分析-》设计-》编码 -》测试-》运行和维护。 软件工程中应用到的图全部有&#xff1a;系统…...

Go 与 Gin 搭建简易 Postman:实现基础 HTTP 拨测的详细指南

Go 与 Gin 搭建简易 Postman&#xff1a;实现基础 HTTP 拨测的详细指南 文章目录 Go 与 Gin 搭建简易 Postman&#xff1a;实现基础 HTTP 拨测的详细指南项目简介代码结构各部分代码功能说明&#xff1a; 代码实现&#xff1a;main.go代码解释 handlers/probe.go代码解释 probe…...

层次原理图

层次原理图简介 层次原理图&#xff08;Hierarchical Schematic&#xff09;是一种常用于电子工程与系统设计的可视化工具&#xff0c;通过分层结构将复杂系统分解为多个可管理的子模块。它如同“设计蓝图”&#xff0c;以树状结构呈现整体与局部的关系&#xff1a;顶层展现系…...

嵌入式硬件篇---拓展板

文章目录 前言 前言 本文简单介绍了拓展板的原理以及使用。...

Redis的主从架构

主从模式 全量同步 首先主从同步过程第一步 会先比较replication id 判断是否是第一次同步假设为第一次同步 那么就会 启动bgsave异步生成RDB 同时fork子进程记录生成期间的新数据发送RDB给从节点 清空本地数据写入RDB 增量同步 对比ReplicationID不同因此选择增量同步在Rep…...

IIS入门指南:原理、部署与实战

引言&#xff1a;Web服务的基石 在Windows Server机房中&#xff0c;超过35%的企业级网站运行在IIS&#xff08;Internet Information Services&#xff09;之上。作为微软生态的核心Web服务器&#xff0c;IIS不仅支撑着ASP.NET应用的运行&#xff0c;更是Windows Server系统管…...

【上位机——WPF】布局控件

布局控件 常用布局控件Panel基类Grid(网格)UniformGrid(均匀分布)StackPanel(堆积面板)WrapPanel(换行面板)DockerPanel(停靠面板)Canvas(画布布局)Border(边框)GridSplitter(分割窗口)常用布局控件 Grid:网格,根据自定义行和列来设置控件的布局StackPanel:栈式面板,包含的…...

使用 C# 入门深度学习:线性代数详细讲解

在深度学习的领域中&#xff0c;线性代数是基础数学工具之一。无论是神经网络的训练过程&#xff0c;还是数据的预处理和特征提取&#xff0c;线性代数的知识都无处不在。掌握线性代数的核心概念&#xff0c;对于理解和实现深度学习算法至关重要。在本篇文章中&#xff0c;我们…...

操作系统之EXT文件系统

1.理解硬件 1.1磁盘、服务器、机柜、机房 机械磁盘是计算机中唯一的一个机械设备 磁盘--- 外设慢容量大&#xff0c;价格便宜 1.1.1光盘 1.1.2服务器 1.1.3机房 1.2磁盘的物理结构 1.3磁盘的存储结构 一个盘片又两个面 每个面都有一个磁头 磁头沿着盘面的半径移动 1.3.1…...

继MCP、A2A之上的“AG-UI”协议横空出世,人机交互迈入新纪元

第一章&#xff1a;AI交互的进化与挑战 1.1 从命令行到智能交互 人工智能的发展历程中&#xff0c;人机交互的方式经历了多次变革。早期的AI系统依赖命令行输入&#xff0c;用户需通过特定指令与机器沟通。随着自然语言处理技术的进步&#xff0c;语音助手和聊天机器人逐渐普…...

Java大厂面试:从Web框架到微服务技术的场景化提问与解析

Java大厂面试&#xff1a;从Web框架到微服务技术的场景化提问与解析 场景&#xff1a; 某知名互联网大厂的面试现场。面试官一脸严肃&#xff0c;对面坐着搞笑的程序员谢飞机。以下是他们的对话&#xff1a; 第一轮&#xff1a;Web框架基础与数据库操作 面试官&#xff1a;谢…...

最新缺陷检测模型:EPSC-YOLO(YOLOV9改进)

目录 引言:工业缺陷检测的挑战与突破 一、EPSC-YOLO整体架构解析 二、核心模块技术解析 1. EMA多尺度注意力模块:让模型"看得更全面" 2. PyConv金字塔卷积:多尺度特征提取利器 3. CISBA模块:通道-空间注意力再进化 4. Soft-NMS:更智能的重叠框处理 三、实…...

leetcode hot100刷题日记——2.字母异位词分组

涉及知识点:vector、哈希表 解答我的解答的时间复杂度分析我的解答的空间复杂度分析复习&#xff1a;排序算法的时间复杂度 和第一题需要的知识点相同&#xff0c;所以知识点复习可见 link1《leetcode hot100刷题日记——1.两数之和》 解题思路&#xff1a;是字母异位词的字符…...

elementUI 单选框存在多个互斥的选项中选择的场景

使用 el-radio-group 来使用单选框组&#xff0c;代码如下&#xff1a; <el-radio-group input"valueChangeHandler" v-model"featureForm.type"><el-radio name"feature" label"feature">业务对象</el-radio><…...

基于区块链技术的智能汽车诊断与性能分析

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 钝感力的“钝”&#xff0c;不是木讷、迟钝&#xff0c;而是直面困境的韧劲和耐力&#xff0c;是面对外界…...

基于区块链技术的供应链溯源系统:重塑信任与透明度

在当今全球化的商业环境中&#xff0c;供应链的复杂性不断增加&#xff0c;产品从原材料采购到最终交付消费者手中的过程涉及多个环节和众多参与者。然而&#xff0c;传统供应链管理面临着诸多挑战&#xff0c;如信息不透明、数据易篡改、追溯困难等&#xff0c;这些挑战不仅影…...