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

redis优化秒杀和消息队列

redis优化秒杀

  • 1. 异步秒杀思路
    • 1.1 在redis存入库存和订单信息
    • 1.2 具体流程图
  • 2. 实现
    • 2.1 总结
  • 3. Redis的消息队列
    • 3.1 基于list实现消息队列
    • 3.2 基于PubSub实现消息队列
    • 3.3 基于stream实现消息队列
      • 3.3.1 stream的单消费模式
      • 3.3.2 stream的消费者组模式
    • 3.4 基于stream消息队列实现异步秒杀

本文为学习redis时做的笔记,学习内容来自 黑马程序员Redis入门到实战教程,该教程是循序渐进的,所以不是一上来就讲完最后的解决方案了,请耐心看完

1. 异步秒杀思路

image.png
这是我们原本的秒杀思路,其中的流程都要经过mysql数据库,而mysql数据库的并发性能不是很好,而且为了避免线程安全问题,还加入了分布式锁,所以整个流程的性能不好,现在我们要去优化它。

我们可以把这整个流程比作一个餐馆点菜的过程,前台点菜并将菜品写在小票上,给顾客一份,后厨一份,后厨根据小票的内容依次做菜

根据这个例子,我们的流程也可以分为两个部分:

  1. 第一部分是判断秒杀资格(判断秒杀库存和校验一人一单)
  2. 第二部分是减库存创建订单流程

两个部分各自为一个线程,主线程判断秒杀资格,如果用户有资格,就开启一个独立线程完成耗时较久的第二部分

image.png
同时,我们也要优化判断秒杀资格的性能,将库存和订单存入redis中,如果判断用户有资格,先将优惠券id,用户id、订单id保存在阻塞队列中,并将订单id返回给用户,用户可以通过这个订单id完成支付操作,虽然此时还没有创建订单,但是在队列中迟早会创建,之后开启独立线程读取队列中的信息,完成下单。
现在的业务流程变成了在redis中判断秒杀资格,保存信息在队列中并返回订单id,性能和吞吐量大大提高

1.1 在redis存入库存和订单信息

现在讨论这两个东西需要什么样的结构去存储

image.png
因为库存只是一个数值,我们使用redis中的string类型去存储,key是优惠券的id,value是库存

到时只需判断库存是否大于0,如果用户有资格,库存要减一,相当于在redis中预减库存

image.png
因为需要一个优惠券id(key)能存很多用户id,而且用户id不能重复,所以订单信息我们使用set结构存储

到时只需看value中是否有该用户id来判断该用户是否下过单

1.2 具体流程图

image.png
为了保证过程的原子性,需要用到lua脚本,通过执行lua脚本后的结果来返回异常信息或者订单id,这样创建订单的时效性就没有那么强了,完全可以照着数据库能承受的范围去执行写的操作,用户只需要订单id就能完成支付操作

2. 实现

需求:
image.png

  1. 新增秒杀优惠券的同时,将优惠券信息保存在redis中

修改添加秒杀优惠券的方法,将优惠券信息保存在redis中

@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(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

添加一个秒杀优惠券

{"title": "120元代金券","beginTime": "2023-11-01T01:11:11","actualValue": 12000,"shopId": 1,"subTitle": "周一至周五均可使用","payValue": 10000,"stock": 100,"endTime": "2024-11-01T01:11:11","type": 1,"rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食"
}

image.png

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

写lua脚本

  • 先写参数列表,再组合成key,最后写逻辑
-- 1. 参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1];
-- 1.2 用户id
local userId = ARGV[2];-- 2. key
-- 2.1 库存key
local stockKey = "seckill:stock" .. voucherId;
-- 2.2 订单key
local orderKey = "seckill:order" .. voucherId;-- 3. 脚本业务
-- 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <= 0) then-- 库存不足return 1
end
-- 判断用户是否下单
if (redis.call('sismember',orderKey,userId) == 1) then-- 用户下过单return 2
end-- 扣减库存
redis.call('incrby',stockKey,-1);
-- 下单,保存用户id到set集合
redis.call('sadd',orderKey,userId);

重写秒杀逻辑

  • 调用lua脚本,根据返回的数字判断,并返回订单号
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;//初始化脚本
static {SECKILL_SCRIPT = new DefaultRedisScript();//读取文件位置,classpath就是resourceSECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);
}@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户id
Long userId = UserHolder.getUser().getId();
// 1. 调用lua脚本
//不需要传key,所以传个空集合
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(),userId.toString()
);
int intValue = result.intValue();//2. 判断结果是否为0
if (intValue != 0) {return Result.fail(intValue == 1 ? "库存不足" : "不要重复下单");
}
//3. 将优惠券id,用户id、订单id保存在阻塞队列中
//TODO 将优惠券id,用户id、订单id保存在阻塞队列中
long orderId = redisIdWorker.nextId("order");//4. 返回订单id
return Result.ok(orderId);
}
  1. 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
//阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

阻塞队列BlockingQueue,当一个线程尝试从一个阻塞队列中获取元素,如果队列中没有元素,这个线程就会被阻塞,当队列中有元素时就会被唤醒并获取元素

seckillVoucher中添加:

//3.2 放入阻塞队列中
orderTasks.add(order);
  1. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
  • 建立线程池,当这个类初始化完线程就去执行VoucherOrderHandler类中的run方法,不断获取队列中的订单信息,去创建订单

@PostConstruct 注解效果是当前类初始化完就去执行

//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
//类加载完就执行
@PostConstruct
private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {//1.获取队列中的订单信息VoucherOrder voucherOrder = orderTasks.take();//2.创建订单handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("订单异常:" + e);}}}
}
  • 执行handleVoucherOrder方法,获取锁,最后执行创建订单方法

注1:在这个方法中不能使用threadlocal去获取用户信息,因为是异步下单,这是一个子线程,不是主线程,没有用户的信息,所以从订单中获取用户id

注2:与上同理,在这个子线程中也无法获取到代理对象,将代理对象设置为成员变量,再从主线程中获取到代理对象

private void handleVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
//创建锁对象
RLock redisLock = redissonClient.getLock("lock:order:" + userId);
//获取锁方法
boolean lockFlag = redisLock.tryLock();
//判断是否获取成功
if (!lockFlag) {log.error("不要重复下单");return;
}//事务方法执行起来可能会出现异常,但最后都要释放锁,所以try-catch起来
try {proxy.createVoucherOrder(voucherOrder);
} finally {redisLock.unlock();
}
}
  • 创建订单逻辑
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
//5.一人一单
Long userId = UserHolder.getUser().getId();
//查询
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
//判断订单是否存在
if (count > 0) {log.error("用户已经购买过一次");return;
}//6.扣减库存
boolean success = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherOrder.getVoucherId())
.gt("stock", 0)
.update();
if (!success) {log.error("不要重复下单");return;
}
save(voucherOrder);
}

2.1 总结

整个流程是:

  • 主线程:

发送请求进入seckillVoucher方法,先判断用户是否有秒杀的资格(通过lua脚本),然后创建订单(将用户id、优惠券id、订单id放进订单里),将订单放入阻塞队列

  • 子线程:

在类初始化的时候去执行线程池,线程池的任务是是不断地从队列中获取订单信息,然后去创建订单。
创建订单先获取锁,再判断一人一单,减库存,最后执行添加订单方法

完整代码:


/*** <p>* 服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private IVoucherOrderService proxy;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;//初始化脚本static {SECKILL_SCRIPT = new DefaultRedisScript();//读取文件位置,classpath就是resourceSECKILL_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();//类加载完就执行@PostConstructprivate void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}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) {Long userId = voucherOrder.getUserId();//创建锁对象RLock redisLock = redissonClient.getLock("lock:order:" + userId);//获取锁方法boolean lockFlag = redisLock.tryLock();//判断是否获取成功if (!lockFlag) {log.error("不要重复下单");return;}//事务方法执行起来可能会出现异常,但最后都要释放锁,所以try-catch起来try {proxy.createVoucherOrder(voucherOrder);} finally {redisLock.unlock();}}@Overridepublic Result seckillVoucher(Long voucherId) {// 获取用户idLong userId = UserHolder.getUser().getId();// 1. 调用lua脚本//不需要传key,所以传个空集合Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());int intValue = result.intValue();//2. 判断结果是否为0if (intValue != 0) {return Result.fail(intValue == 1 ? "库存不足" : "不要重复下单");}//3. 将优惠券id,用户id、订单id保存在阻塞队列中//3.1.创建订单VoucherOrder order = new VoucherOrder();//3.1.1 订单idlong orderId = redisIdWorker.nextId("order");order.setId(orderId);//3.1.2 用户idorder.setUserId(userId);//3.1.3 优惠券idorder.setVoucherId(voucherId);//3.2 放入阻塞队列中orderTasks.add(order);//3.3 获取当前的代理对象(事物)proxy = (IVoucherOrderService) AopContext.currentProxy();//4. 返回订单idreturn Result.ok(orderId);}@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {//5.一人一单Long userId = voucherOrder.getUserId();//查询Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();//判断订单是否存在if (count > 0) {log.error("用户已经购买过一次");return;}//6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();if (!success) {log.error("不要重复下单");return;}save(voucherOrder);}}

测试:
image.png

image.png
image.png

现在我们的阻塞队列使用的是jvm的内存,将来有无数的请求进来,内存可能会满,而且如果服务重启或者宕机,订单信息就消失了,可能会出现一些问题,接下来学习一下redis的消息队列

3. Redis的消息队列

消息队列,字面意思就是存放消息的队列,最简单的消息队列包含三个角色

  • 生产者:发送消息到消息队列
  • 消息队列:存储和管理消息,也称为消息代理
  • 消费者:从消息队列获取消息并处理消息

image.png

市面上有很多的消息队列的产品,但是搭建他们也是需要成本的,既然我们已经搭建起了redis集群,为了减少成本,可以使用redis提供的三种不同的方式

  • list结构:基于list结构模拟消息队列
  • PubSub:订阅发布,基本的点对点消息模型
  • Stream:比较完善的消息队列模型

3.1 基于list实现消息队列

Redis的list数据结构是一个双向链表,很容易模拟出队列效果。
队列是入口和出口不在一边,因此我们可以利用: LPUSH 结合 RPOP、或者 RPUSH 结合LPOP来实现。
不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像VM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
image.png

优点:

  • 利用Redis存储,不受限于JVM内存上限
  • 基于Redis的持久化机制,数据安全性有保证
  • 可以满足消息有序性

缺点:

  • 无法避免消息丢失
  • 只支持单消费者

3.2 基于PubSub实现消息队列

Pubsub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息

  • SUBSCRIBE channel[channell : 订阅一个或多个频道
  • PUBLISH channel msg : 向一个频道发送消息
  • PSUBSCRIBE pattern[pattern]: 订与pattern格式匹配的所有频道

image.png

优点:

  • 采用发布订阅模型,支持多生产、多消费

缺点:

  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失

3.3 基于stream实现消息队列

3.3.1 stream的单消费模式

Stream是Redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列

  1. 发送消息的命令:

image.png

  • 中间的内容都是可选。红框标注的基本可以不用管;绿框用来设置消息队列的最大消息数量;黄框用来设置消息的id,*代表redis自动生成;蓝框是队列中的消息内容

示例:
xadd users * name zhuyi love lvhan
返回id
image.png

  1. 查看队列中消息数量的命令

xlen key
示例:
image.png

  1. 读取队列中消息的命令

image.png

  • [COUNT count] 是每次读取消息的最大数量
  • [BLOCK milliseconds] 当没消息时,是否阻塞及阻塞时长
  • STREAMS key 要从哪个队列开始读,key就是队列名
  • ID 起始id,只返回大于该id的消息。0:代表从第一个消息开始,$:代表从最新的消息开始

示例:
从第一个消息开始读
image.png
因为消息已经读过,没有最新的消息,所以读不出来
image.png

特点:

  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 可以阻塞读取
  • 有消息漏读的风险。使用$,在读一条消息的时候,有超过一条以上的消息进入队列,只会读取最后一条

3.3.2 stream的消费者组模式

消费者组,将多个消费者划分到一个组中,监听同一个队列,有以下好处:

  • 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度。如果想让一个消息被多个消费者消费,可以多加几个组
  • 消息标识:消费者组会维护一个标识,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标识之后读取消息,确保每一个消息都会被消费
  • 消息确认:消费者获取消息后,消息处于pending(待处理)状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除,保证所有的消息只要被获取到了,就能至少被消费一次
  1. 创建消费者组:

image.png

  • key:队列名称
  • groupName:消费者组名称
  • ID:起始ID标识,$代表队列中最新的消息,0则代表队列中第一个消息
  • MKSTREAM:队列不存在时自动创建队列

其他常见命令:
image.png

示例:
image.png

  1. 从消费者组读取消息

image.png

  • group:消费组名称
  • consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
  • count:本次查询的最大数量
  • BLOCK millisecond:当没有消息时最长等待时间
  • NOACK:无需手动ACK,获取到消息后自动确认
  • STREAMS key:指定队列名称
  • ID:获取消息的起始id:1. “>”:从下一个未消费的消息开始。2. 其他:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始

示例:
image.png

  1. 确认消息

我们获取到消息消费后,一定要确认它,把他从pending-list中移除
image.png

  • key:队列名称
  • group:消费组名称
  • ID:获取消息的起始id:1. “>”:从下一个未消费的消息开始。2. 其他:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始

示例:
image.png

消费者监听消息的基本思路:
image.png
特点:

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

如果你的公司业务比较庞大,对消息队列要求比较严格,还是建议使用更专业的消息队列,如rabbitmq等,但如果是中小型公司,对消息队列需要没那么大,redis的stream就已经能满足需求了

3.4 基于stream消息队列实现异步秒杀

image.png

  1. 创建一个Stream类型的消息队列,定义为stream.orders,这里直接在客户端完成了
XGROUP CREATE stream.orders g1 0 mkstream
  1. 修改之前的秒杀下单lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包括voucherId、userId、orderId

修改lua脚本:
主要添加了一个订单id的参数,在业务的最后向队列发送消息

-- 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. 脚本业务
-- 判断库存是否充足
if (tonumber(redis.call('get',stockKey)) <= 0) then-- 库存不足return 1
end
-- 判断用户是否下单
if (redis.call('sismember',orderKey,userId) == 1) then-- 用户下过单return 2
end-- 扣减库存
redis.call('incrby',stockKey,-1);
-- 下单,保存用户id到set集合
redis.call('sadd',orderKey,userId);
-- 发送消息
redis.call('xadd','stream.orders','*','voucherId',voucherId,'userId',userId,'id',orderId)
return 0

修改一下调用lua脚本的逻辑:
新添加一个订单id的参数

// 1. 调用lua脚本
//不需要传key,所以传个空集合
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(),String.valueOf(orderId)
);
int intValue = result.intValue();
  1. 项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单

在原有的线程任务逻辑上修改,从消息队列中获取订单信息,判断一下订单信息是否为空,如果为空,说明没有消息,继续下一次循环,如果有,去解析数据,拿到订单,通过以前写过的createVoucherOrder()方法来创建订单,最后一定要确认消息,将消息从pending-list中移除。
如果在执行时出现了错误或者服务宕机,通过handlePendingList()方法处理pending-list中已消费但未确认的订单,这里如果出现异常,就不用再调用这个方法了

private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));// 2.判断订单信息是否为空if (list == null || list.isEmpty()) {// 如果为null,说明没有消息,继续下一次循环continue;}// 解析数据MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);// 3.创建订单handleVoucherOrder(voucherOrder);// 4.确认消息 XACKstringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());} catch (Exception e) {log.error("处理订单异常", e);handlePendingList();}}}private void handlePendingList() {while (true) {try {// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create("stream.orders", ReadOffset.from("0")));// 2.判断订单信息是否为空if (list == null || list.isEmpty()) {// 如果为null,说明没有异常消息,结束循环break;}// 解析数据MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);// 3.创建订单handleVoucherOrder(voucherOrder);// 4.确认消息 XACKstringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());} catch (Exception e) {log.error("处理订单异常", e);}}}
}

XACK确认消息:
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());

相关文章:

redis优化秒杀和消息队列

redis优化秒杀 1. 异步秒杀思路1.1 在redis存入库存和订单信息1.2 具体流程图 2. 实现2.1 总结 3. Redis的消息队列3.1 基于list实现消息队列3.2 基于PubSub实现消息队列3.3 基于stream实现消息队列3.3.1 stream的单消费模式3.3.2 stream的消费者组模式 3.4 基于stream消息队列…...

arm-eabi-gcc 和 arm-none-eabi-gcc 都是基于 GCC 的交叉编译器

arm-eabi-gcc 和 arm-none-eabi-gcc 都是基于 GCC 的交叉编译器&#xff0c;用于编译 ARM 架构的嵌入式系统。它们的命名规则如下&#xff1a; arm 表示目标架构是 ARM。eabi 表示嵌入式应用程序二进制接口&#xff08;Embedded Application Binary Interface&#xff09;&…...

《大话设计模式》(持续更新中)

《大话设计模式》 序 为什么要学设计模式第0章 面向对象基础什么是对象&#xff1f;什么是类&#xff1f;什么是构造方法&#xff1f;什么是重载&#xff1f;属性与字段有什么区别&#xff1f;什么是封装&#xff1f;什么是继承&#xff1f;什么是多态&#xff1f;抽象类的目的…...

人工智能原理复习--绪论

文章目录 人工智能原理概述图灵测试人工智能的研究方法符号主义连接主义行为主义总结 人工智能原理概述 人工智能是计算机科学基础理论研究的重要组成部分 现代人工智能一般认为起源于美国1956你那夏季的达特茅斯会议&#xff0c;在这次会议上&#xff0c;John McCarthy第一次…...

[网络] 字节一面~ 2. HTTP 2 与 HTTP 1.x 有什么区别

头部压缩 在 HTTP2 当中&#xff0c;如果你发出了多个请求&#xff0c;并且它们的头部(header)是相同的&#xff0c;那么 HTTP2 协议会帮你消除同样的部分。(其实就是在客户端和服务端维护一张索引表来实现)二进制格式 HTTP1.1 采用明文的形式 HTTP/2 全⾯采⽤了⼆进制格式&…...

自己动手实现一个深度学习算法——八、深度学习

深度学习是加深了层的深度神经网络。 1.加深网络 1&#xff09;向更深的网络出发 创建一个如下图所示的网络结构的CNN 这个网络的层比之前实现的网络都更深。这里使用的卷积层全都是33 的小型滤波器&#xff0c;特点是随着层的加深&#xff0c;通道数变大&#xff08;卷积…...

js闭包的必要条件及创建和消失(生命周期)

>创建闭包的必要条件&#xff1a; 1.函数嵌套 2.内部函数引用外部函数的变量 3.将内部函数作为返回值返回 >闭包是什么&#xff1f; 就是可以访问外部函数&#xff08;作用域&#xff09;中变量的内部函数 > 闭包是什么时候产生的&#xff1f; - 当调用外部函数…...

鸿蒙开发-ArkTS 语言-基础语法

[写在前面: 文章多处用到gif动图&#xff0c;如未自动播放&#xff0c;请点击图片] 1. 初识 ArkTS 语言 ArkTS 是 HarmonyOS 优选主力开发语言。ArkTS 是基于 TypeScript (TS) 扩展的一门语言&#xff0c;继承了 TS 的所有特性&#xff0c;是TS的超集。 主要是扩展了以下几个方…...

GPT实战系列-GPT训练的Pretraining,SFT,Reward Modeling,RLHF

GPT实战系列-GPT训练的Pretraining&#xff0c;SFT&#xff0c;Reward Modeling&#xff0c;RLHF 文章目录 GPT实战系列-GPT训练的Pretraining&#xff0c;SFT&#xff0c;Reward Modeling&#xff0c;RLHFPretraining 预训练阶段Supervised FineTuning &#xff08;SFT&#x…...

电子学会C/C++编程等级考试2022年03月(三级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:和数 给定一个正整数序列,判断其中有多少个数,等于数列中其他两个数的和。 比如,对于数列1 2 3 4, 这个问题的答案就是2, 因为3 = 2 + 1, 4 = 1 + 3。 时间限制:10000 内存限制:65536输入 共两行,第一行是数列中数的个数…...

理解 JUnit, JaCoCo 到 SonarQube 的过程及 Maven 配置

Java 项目需要产生单元测试及代码覆盖率的话一直都是走的 JUnit 单元测试&#xff0c;JaCoCo 基于测试产生测试覆盖率&#xff0c;然后送到 SonarQube 去展示这条路子。当然 SonarQube 还可以帮我们进行代码的静态分析。但对其中的具体使用及过程知晓的并不深&#xff0c;基本就…...

人工智能关键技术决定机器人产业的前途

人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;是指让计算机或机器具有类似于人类的智能和学习能力的技术。人工智能技术与机器人技术的结合将改变传统的机器人行业格局&#xff0c;就像智能手机对传统手机的颠覆一样。本文从人工智能技术的发展趋势、…...

2023华为ICT网络初赛试题回顾

所有题目都只能用来学习交流&#xff0c;禁止用于非法不公平的使用&#xff0c;如有侵权&#xff0c;该文章立刻删除。 1、某机房没有合适长度的网线&#xff0c;现需手工制作一个568B标准的双纹线&#xff0c;那么应按照以下哪一线序进行制作? A.绿白,绿&#xff0c;蓝&#…...

Hands-on Machine Learning with Scikit-Learn,Keras TensorFlow

读书记录(缓慢更新) 目录 Part 1. The Fundamentals of Machine Learning The Content of The Machine Learning Landscape The Machine Learning Landscape Part 1. The Fundamentals of Machine Learning The Content of The Machine Learning Landscape Part 1. The F…...

242. 有效的字母异位词

这篇文章会收录到 :算法通关村第十二关-白银挑战字符串经典题目-CSDN博客 242. 有效的字母异位词 描述 : 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xff1a;若 s 和 t 中每个字符出现的次数都相同&#xff0c;则称 s 和 t …...

TUP通信——与多个客户端同时通信

一&#xff0c;概括&#xff1a;可以通过多线程思想每加一个客户端由线程池中的主线程交给一个子线程管理 二&#xff0c;案例 &#xff08;1&#xff09;&#xff0c;线程池 &#xff08;2&#xff09;&#xff0c;服务端 &#xff08;3&#xff09;&#xff0c;客户端...

基于helm的方式在k8s集群中部署gitlab - 备份恢复(二)

接上一篇 基于helm的方式在k8s集群中部署gitlab - 部署&#xff08;一&#xff09;&#xff0c;本篇重点介绍在k8s集群中备份gitlab的数据&#xff0c;并在虚拟机上部署相同版本的gitlab&#xff0c;然后将备份的数据进行还原恢复 文章目录 1. 备份2. 恢复到虚拟机上的gitlab2.…...

B树与B+树的对比

B树&#xff1a; m阶B树的核心特性&#xff1a; 树中每个节点至多有m棵子树&#xff0c;即至多含有m-1个关键字根节点的子树数属于[2, m]&#xff0c;关键字数属于[1, m-1]&#xff0c;其他节点的子树数属于 [ ⌈ m 2 ⌉ , m ] [\lceil \frac{m}{2}\rceil, m] [⌈2m​⌉,m]&am…...

关键路径-STL版/拓扑排序 关键路径【数据结构】

关键路径-STL版 题目描述 给定有向图无环的边信息&#xff0c;求每个顶点的最早开始时间、最迟开始时间。 输入 第一行图的顶点总数 第二行边的总数 第三行开始&#xff0c;每条边的时间长度&#xff0c;格式为源结点 目的结点 长度 输出 第一行&#xff1a;第个顶点的最早…...

最新AI创作系统ChatGPT系统运营源码,支持GPT-4图片对话能力,上传图片并识图理解对话,支持DALL-E3文生图

一、AI创作系统 SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&#xff01;本系统使用NestjsVueTypescript框架技术&#xff0c;持续集成AI能力到本系统。支持OpenAI DALL-E3文生图&#xff0c;…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案

问题描述&#xff1a;iview使用table 中type: "index",分页之后 &#xff0c;索引还是从1开始&#xff0c;试过绑定后台返回数据的id, 这种方法可行&#xff0c;就是后台返回数据的每个页面id都不完全是按照从1开始的升序&#xff0c;因此百度了下&#xff0c;找到了…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...

听写流程自动化实践,轻量级教育辅助

随着智能教育工具的发展&#xff0c;越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式&#xff0c;也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建&#xff0c;…...