7.使用Redis进行秒杀优化

目录

1. 优化思路

总结之前实现的秒杀过程

下单流程

2. 使用Redis完成秒杀资格判断和库存

0. Redis中数据类型的选用

1.将优惠券信息保存到Redis中

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

3. 开启新协程,处理数据库的数据

4.可以使用消息队列

3. Redis消息队列

基于List实现消息队列

基于 Pub-Sub 模式 

其在处理消息积压时,为什么会丢数据?

基于Stream的消息队列

发送消息:XADD

读取消息:XREAD

基于Stream的消息队列-消费者组

 1. 创建消费者组 

​编辑

2. 从消费者组读取消息

使用stream消息队列来消费订单

1. 创建stream类型的消息队列 

2. 往队列中添加订单消息

3. 获取消息


项目地址:https://github.com/liwook/PublicReview

1. 优化思路

总结之前实现的秒杀过程

  1. 最开始我们的遇到自增ID问题,我们通过实现分布式ID解决了问题;
  2. 之后在单体系统下遇到了一人多单超卖问题,我们通过乐观锁解决,判断库存是否大于0,大于0才返回成功;
  3. 我们对业务进行了变更,将一人多单变成了一人一单,结果在高并发场景下同一用户发送相同请求仍然出现了超卖问题,我们通过悲观锁解决了,在查看库存时候就添加锁,其他线程的同一个用户就会被堵住
  4. 由于用户量的激增,我们将单体系统升级成了集群,结果由于锁只能在一个进程中可见导致又出现了同一用户发送下单请求出现超卖问题。我们通过实现分布式锁成功解决集群下的超卖问题。
  5. 最后,我们直接使用现有成熟的方案redsync来解决上诉出现的所有问题。什么不可重试、不可重入、超时释放、原子性等问题,redsync都提供相对应的解决方法。

上面的都是我们为了解决程序可用所做的。现在程序执行不会出现超卖问题的。但是其性能可能不太好。

下单流程

查询优惠卷 – 判断秒杀库存是否足够 –加分布式锁 –查询订单校验是否是一人一单扣减库存创建订单

在这7步操作中,加粗黑体的操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的比较慢,所以我们需要异步程序执行。 

我们可以将一部分的工作交给Redis,并且通过开启一个独立的子线程去异步执行数据库的一些操作,从而大大提高效率。

2. 使用Redis完成秒杀资格判断和库存

把 判断秒杀库存 和 检验一人一单 交由Redis处理。Redis中判断能抢购,就返回成功,之后再开启一个线程去处理一些MySql操作。

  1. 当用户下单之后,判断库存是否充足只需要在redis中根据key找对应的value是否大于0即可。
  2. 如果不充足,则直接结束;如果充足,继续在redis中判断用户是否可以下单。

0. Redis中数据类型的选用

  • 对于库存值,我们可以直接使用 String 类型
  • 一人一单的判断,一个优惠卷是可以让多个不同的用户购买的,即一个优惠卷可以对应多个用户,就是说可以用个集合,key是voucherId,而集合元素就是userId。而需要可以快速判断该集合中是否有某userId,那可以使用 Set类型key是优惠卷idvalue是userId

所以:Redis中,库存使用String 类型,判断一人一单使用Set类型。

检验一人一单的过程:

  • 如果set集合中没有这条数据,说明他可以下单;
  • 如果set集合中没有这条记录,则将userId存入到redis中。

该过程是两个操作,​​​​​而且我们还需要判断库存,所以需要使用lua来保证整个过程的原子性。

基于以上的分析,可得出需求:

  1. 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
  2. 基于Lua脚本,判断秒杀库存该用户是否已购买过,决定用户是否抢购成功
  3. 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
  4. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

但是在go语言中,开启一个协程是很方便的,消耗的资源也少,所以当前,处理完redis中的数据,直接开启一个新协程去处理数据库中的数据。

1.将优惠券信息保存到Redis中

const (SeckillStockKey = "seckill:voucher:"
)// 添加秒杀券
func addSeckillVoucher(voucher Voucher) error {.....................q := query.Use(db.DBEngine)//使用事务err = q.Transaction(func(tx *query.Query) error {..........................})if err != nil {return err}//往redis中添加秒杀卷的库存return db.RedisClient.Set(context.Background(), SeckillStockKey+strconv.Itoa(int(v.ID)), voucher.Stock, 0).Err()
}

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

在internal/shopservice添加lau.go文件。

const AdjustSeckill = `
-- 秒杀优化需求二:基于Lua脚本,判断秒杀库存、一人一单,决定用户是否有购买资格
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = KEYS[1]
-- 1.2.用户id
local userId = KEYS[2]-- 2.数据key
-- 2.1.库存key  ..lua的字符串拼接
local stockKey = 'seckill:stock:' .. voucherId-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey  tonumber()将字符串转换为数字
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.库存不足,返回1return 1
end
-- 3.2.判断用户是否下单 SISMEMBER:判断set集合中是否存在某个元素
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,说明是重复下单,返回2return 2
end
-- 3.4.扣库存 incrby stockKey -1redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户) sadd orderKey userIdredis.call('sadd', orderKey, userId)return 0
`

修改seckillVoucher函数。注意:返回的结果res的类型是interface{},需要转换。

// post /voucher/seckill
func SeckillVoucher(c *gin.Context) {var req seckillResquesterr := c.BindJSON(&req)if err != nil {slog.Error("SeckillVoucher, bind json bad", "err", err)code.WriteResponse(c, code.ErrBind, nil)return}script := redis.NewScript(AdjustSeckill)res, err := script.Run(context.Background(), db.RedisClient, []string{strconv.Itoa(req.VoucherId), strconv.Itoa(req.UserId)}).Result()if err != nil {slog.Error("run script bad", "err", err)code.WriteResponse(c, code.ErrDatabase, nil)return}//需要注意,res的类型是interface{},需要转换。if res.(int64) != 0 {// if res != 0 {var e stringif res == 1 {e = "stock not enough"} else {e = "order already exist"}code.WriteResponse(c, code.ErrDatabase, e)}orderId := NextId("order")//TODO 开启新协程,异步更新数据库go createVoucherOrder(req, orderId)// 3.返回订单idcode.WriteResponse(c, code.ErrSuccess, gin.H{"orderId": orderId})
}

 测试效果符合预期,而且花费的时间也大大减少了。开启400个协程来模拟,结果只需要51ms,比之前的2,3s快了很多。

3. 开启新协程,处理数据库的数据

协程函数是之前的CreateVoucherOrder函数。

这里有个问题是:我们不知道是否成更新了MySql中的数据。这个是一定要处理的。看后续。

// post /voucher/seckill
func SeckillVoucher(c *gin.Context) {............orderId := NextId("order")//开启新协程,异步更新数据库go createNewOrder(req, orderId)// 3.返回订单idcode.WriteResponse(c, code.ErrSuccess, gin.H{"orderId": orderId})
}//该函数就是在原来的createVoucherOrder函数上进行修改的
func createNewOrder(req seckillResquest, orderId int64) {order := model.TbVoucherOrder{ID:        orderId,VoucherID: uint64(req.VoucherId),UserID:    uint64(req.UserId),}//处理两张表(订单表,秒杀卷表),使用事务q := query.Use(db.DBEngine)q.Transaction(func(tx *query.Query) error {//3.合法,库存数量减1//使用update,要是没有该条数据,不会返回gorm.ErrRecordNotFound或者有错误的。info, err := tx.TbSeckillVoucher.Where(tx.TbSeckillVoucher.VoucherID.Eq(uint64(req.VoucherId)), tx.TbSeckillVoucher.Stock.Gt(0)).UpdateSimple(tx.TbSeckillVoucher.Stock.Add(-1))if err != nil {return err}if info.RowsAffected == 0 {return fmt.Errorf("affected rows is 0")}//4.成功,创建对应的订单,并保存到数据中// err = tx.TbVoucherOrder.Create(&order)err = tx.TbVoucherOrder.Select(tx.TbVoucherOrder.ID, tx.TbVoucherOrder.VoucherID, tx.TbVoucherOrder.UserID).Create(&order)if err != nil {return err}return nil})
}

那么每来一个抢单,就需要开启一个新协程。会有几个问题:

  • 资源管理:如果并发量非常大,频繁地开启新协程可能会消耗过多的系统资源,如内存等。如果没有合理的协程数量控制机制,可能会导致系统资源耗尽。
  • 错误处理复杂:在新协程中处理 MySQL 写入时,错误处理会变得比较复杂。例如,如果写入 MySQL 失败,可能需要一种机制来通知相关模块或者进行重试操作,并且需要确保这些操作不会影响到主业务流程。
  • 数据安全问题,要是程序某些原因宕机了,而没有保存到。

 那么可以使用消息队列。

4.可以使用消息队列

 消息队列即是存放消息的队列。最简单的消息队列模型包括3个角色:

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

使用队列的好处在于 解耦:所谓解耦在我们秒杀中就是:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。 但是这也会导致一个问题,就是我们添加了一个中间件,就要保证该中间件的高可用等等需求。

3. Redis消息队列

Redis中可用作消息队列的有3种:List数据结构,基于PubSub,Stream数据结构。

基于List实现消息队列

Redis的list数据结构是一个双向链表,很容易模拟出队列效果。

队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。
不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果

  • 生产消息:BRPUSH key value [value ...]         将一个或多个元素推入到指定列表的头部。如果列表不存在,BRPUSH命令会自动创建一个新的列表
  • 消费消息:BRPOP key [key ...] timeout      从指定的一个或多个列表中弹出最后一个元素。如果 list 列表为空,BRPOP命令会导致客户端阻塞,直到有数据可用或超过指定的超时时间。

优点:

  • 利用 Redis 存储,不受go程序内存上限。消费者可以积压消息,不会因为客户端的消息过多而被强行断开。
  • 基于 Redis 的持久化机制,数据安全有保证
  • 可以满足消息有序性

缺点:

  • 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  • 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了(因为从 List 中 POP 一条消息出来后,这条消息就会立即从链表中删除了。也就是说,无论消费者是否处理成功,这条消息都没办法再次消费了)

基于 Pub-Sub 模式 

这是Redis2.0版本提供的,其提供了以下命令来完成发布、订阅的操作:

# 用于向指定频道发布一条消息
PUBLISH channel message # 订阅一个或多个频道
SUBSCRIBE channel [channel]
# 用于取消订阅一个或多个频道
UNSUBSCRIBE [channel [channel ...]]
# 用于订阅一个或多个符合给定模式的频道,接收消息
PSUBSCRIBE pattern [pattern ...]
# 用于取消订阅一个或多个符合给定模式的频道
PUNSUBSCRIBE [pattern [pattern ...]]

消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。 

那么,在程序中消费者就订阅某个channel,生产者在处理好redis后,把订单等信息往channel中发布,那消费者就可以收到从而进行处理。

优点:

  • 支持多生产、多消费者

缺点: 

  • Pub/Sub 在实现时非常简单,它既不基于任何数据类型,也不进行任何数据存储。所以无法持久化保存消息(不会写入到 RDB 和 AOF 中),如果 Redis 服务器宕机或重启,那么所有的消息将会丢失;
  • 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后就不能消费之前的历史消息。因为下线期间生产者发布的消息会因找不到消费者而被丢弃。
  • 消息堆积,缓冲区溢出,消费者会被强制踢下线,数据也会丢失。

其在处理消息积压时,为什么会丢数据?

  • 采用 list 当作队列,消息积压时会导致链表很长,Redis 内存持续增长,直到消费者取出数据,list 属于 拉 模型,数据可一直积压在内存中。
  • 而 Pub/Sub 属于 推 模型,消费者订阅队列时,Redis 会在 Server 上为其分配一个缓冲区,生产者发布消息时先写入该缓冲区,消费者从缓冲区读取消息。
  • 但缓冲区有上限,若消费者拉取消息很慢,会造成消息积压,缓冲区内存持续增长。若超过缓冲区配置上限,Redis 会强制把消费者踢下线,导致消费失败和数据丢失。

基于Stream的消息队列

在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式 PubSub,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • 列表实现消息队列的方式不能重复消费,一个消息消费完就会被删除;
  • 有序集合消息队列的实现方式不能存储相同 value 的消息,并且不能阻塞读取消息。

并且以上三种方式在实现消息队列时,只能存储单 value 值,也就是如果你要存储一个对象的情况下,必须先序列化成 JSON 字符串,在读取之后还要反序列化成对象才行,这也给用户的使用带来的不便。

基于以上问题,Redis 5.0 便推出了 Stream 类型,用于完美地实现消息队列,它借鉴了 Kafka 的设计思路,它支持消息的持久化和消息轨迹的消费,支持 ack 确认消息的模式,让消息队列更加的稳定和可靠。

发送消息:XADD

其结果是返回该消息的唯一id。这里的示例就是发送了一个对象,Json格式。

读取消息:XREAD

XREAD阻塞方式,读取最新的消息: 

在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果。

注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

STREAM类型消息队列的XREAD命令特点:

  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 可以阻塞读取
  • 有消息漏读的风险

而steam有消息者组模式,可以解决上述的一些缺陷。

基于Stream的消息队列-消费者组

消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。

消费者组的特点:

  1. 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
  2. 消息标识:消费者组会维护一个标识,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标识之后读取消息。确保每一个消息都会被消费。
  3. 消息确认:消费者获取消息后,那该消息就处于pending(待处理)状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除。

相关语法: 

// 创建消费者组
XGROUP CREATE key groupName ID[MKSTREAM]//  删除指定的消费者组
XGROUP DESTORY key groupName// 给指定的消费者组添加消费者
XGROUP CREATECONSUMER key groupname consumername// 删除消费者组中的指定消费者
XGROUP DELCONSUMER key groupname consumername// 从消费者组读取消息
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]ex:XPENDING mystream group55 - + 10  // - + 表示all// 确认消息
XACK key group id [id ...]// 获取pending-list的消息
XPENDING key group [[IDLE min-idle-time] start end count [consumer]]//- IDLE:确认时间//- start&end:确认消息的起始和末尾// - count:确认数量
 1. 创建消费者组 
2. 从消费者组读取消息

使用stream消息队列来消费订单

需求:

  1. 创建一个Stream类型的消息队列,名为stream.orders
  2. 修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId
  3. 项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单

1. 创建stream类型的消息队列 

第一步,我们直接在命令行行执行即可。 

2. 往队列中添加订单消息

之前我们判断是否有抢购资格时候是用Lua脚本,所以我们也可以把 往对列中添加消息 这操作添加到Lua脚本中。所以这次Lua脚本中需要添加orderid。

#在对应位置添加-- 1.3.订单id
local orderId = KEYS[3]-- 2.2 订单key
local orderKey = 'seckill:order:' .. voucherId;-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)

那seckillVoucher函数中的Run代码就需要传递三个参数。

// post /voucher/seckill
func SeckillVoucher(c *gin.Context) {var req seckillResquesterr := c.BindJSON(&req)if err != nil {...................return}orderId := NextId("order")script := redis.NewScript(AdjustSeckill)// res, err := script.Run(context.Background(), db.RedisClient, []string{strconv.Itoa(req.VoucherId), strconv.Itoa(req.UserId)}).Result()res, err := script.Run(context.Background(), db.RedisClient, []string{strconv.Itoa(req.VoucherId), strconv.Itoa(req.UserId), strconv.Itoa(int(orderId))}).Result()if err != nil {slog.Error("run script bad", "err", err)code.WriteResponse(c, code.ErrDatabase, nil)return}................// 开启新协程,异步更新数据库// go createVoucherOrder(req, orderId)// 3.返回订单idcode.WriteResponse(c, code.ErrSuccess, gin.H{"orderId": orderId})
}

3. 获取消息

往队列中发送消息后,就需要获取消息。在internal目录中创建mq目录,在该目录创建stream.go。

const (streamName      = "stream.orders"streamGroupName = "group1"
)func StartStream() {// 创建消费组(如果不存在)err := db.RedisClient.XGroupCreateMkStream(context.Background(), streamName, streamGroupName, "0").Err()if err != nil && err.Error() != "BUSYGROUP Consumer Group name already exists" {slog.Error("Failed to create consumer group", "err", err)panic(err)}// 从消费组中读取消息for i := 1; i <= 5; i++ {name := "consumer" + fmt.Sprint(i)go startStream(name)}
}//在main函数中使用
//main.go
func main() {mq.StartStream() //开启redis的stream队列获取消息r := router.NewRouter().............
}

创建消费者后,然后开启获取消息。

  • 可以开多个协程去获取,每一个协程就代表一个消费者。
  • 使用go-redis的.XReadGroup函数获取消息。消费成功后,使用XAck函数来标记消息为已处理。
  • 若其中创建订单错误就再尝试一次,要是再失败,就发送邮件进行人工处理,或者发送到死信队列
func startStream(name string) {// 从消费组中读取消息for {msgs, err := db.RedisClient.XReadGroup(context.Background(), &redis.XReadGroupArgs{Group:    streamGroupName,Consumer: name,Streams:  []string{streamName, ">"}, //streamName:这是要读取的 Redis Stream 的名称。表示从这个特定的 Stream 中读取消息。//">":这个特殊的标识符在 Redis Stream 中用于表示从 Stream 的末尾开始读取,即只读取尚未被任何消费者处理的新消息。Count: 1,Block: 0,}).Result()if err != nil {slog.Error("Failed to read messages from stream", "err", err)continue}//处理信息if len(msgs) == 0 {continue}msg := msgs[0].Messages[0]fmt.Printf("Received message: %v", msg.Values)voucherId := msg.Values["voucherId"].(string)userId := msg.Values["userId"].(string)orderId := msg.Values["id"].(string)voucherIdInt, _ := strconv.Atoi(voucherId)userIdInt, _ := strconv.Atoi(userId)orderIdInt, _ := strconv.Atoi(orderId)err = createOrder(voucherIdInt, userIdInt, orderIdInt)if err != nil {slog.Error("Failed to create voucher order", "err", err)//再次尝试err = createOrder(voucherIdInt, userIdInt, orderIdInt)if err != nil {// 发送邮件让人工处理。或者发送到死信队列body := "voucherId:" + voucherId + ", userId:" + userId + ", orderId:" + orderIdmail.SendMail(*config.MailOption, body)}}//确认消息,发送ackdb.RedisClient.XAck(context.Background(), streamName, streamGroupName, msgs[0].Messages[0].ID)}
}

创建订单的函数createOrder

//stream.go
func createOrder(voucherId int, userId int, orderId int) error {order := model.TbVoucherOrder{ID:        int64(orderId),VoucherID: uint64(voucherId),UserID:    uint64(userId),}//处理两张表(订单表,秒杀卷表),使用事务q := query.Use(db.DBEngine)return q.Transaction(func(tx *query.Query) error {//3.合法,库存数量减1//使用update,要是没有该条数据,不会返回gorm.ErrRecordNotFound或者有错误的。info, err := tx.TbSeckillVoucher.Where(tx.TbSeckillVoucher.VoucherID.Eq(uint64(voucherId)), tx.TbSeckillVoucher.Stock.Gt(0)).UpdateSimple(tx.TbSeckillVoucher.Stock.Add(-1))if err != nil {return err}if info.RowsAffected == 0 {return fmt.Errorf("affected rows is 0")}//4.成功,创建对应的订单,并保存到数据中return tx.TbVoucherOrder.Select(tx.TbVoucherOrder.ID, tx.TbVoucherOrder.UserID, tx.TbVoucherOrder.VoucherID).Create(&order)})
}

发送邮件相关的代码

在pkg目录创建mail目录,创建mail.go文件,编写关于邮件的代码。之后在config.go文件添加相应的变量,并解析配置文件的关于mail的参数。

type MailSetting struct {Host     stringPort     intUsername stringPassword stringTo       []string
}func SendMail(mailConn MailSetting, body string, attachment ...string) error {m := gomail.NewMessage()// m.SetHeader("From", m.FormatAddress(mailConn.User, mailConn.Alias)) //设置邮件别名m.SetHeader("From", mailConn.Username) //设置邮件别名m.SetHeader("To", mailConn.To...)      //发送给多个用户m.SetHeader("Subject", "需要人工处理的订单")    //设置邮件主题m.SetBody("text/plain", body)          //设置邮件正文if len(attachment) > 0 {for _, v := range attachment {m.Attach(v) // 附件文件,可以是文件,照片,视频等等}}d := gomail.NewDialer(mailConn.Host, mailConn.Port, mailConn.Username, mailConn.Password)// 关闭SSL协议认证d.TLSConfig = &tls.Config{InsecureSkipVerify: true}return d.DialAndSend(m)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/462473.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

终于把DETR搞懂了!Detection Transformer架构详解及使用方法说明

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

Calling short variants with GATK4

计算生物学实验5: Calling short variants with GATK4 1. 实验目的 本实验目的是利用 GATK4 工具准确高效地检测出基因组中的短变异。通过该工具对样本基因组进行分析&#xff0c;旨在发现单核苷酸变异&#xff08;SNV&#xff09;和小的插入缺失&#xff08;Indel&#xff0…

S32K324 DTCM/DTCM Backdoor使用及测试

文章目录 前言S32K324的Memory mapDTCM的原理DTCM的使用DTCM/DTCM backdoor测试总结 前言 S32K324的Ram在选型手册上给的是512K&#xff0c;但实际上sram只有320k,项目中对ram的需求更大&#xff0c;所以需要拓展一下ram的使用。本文分析DTCM的使用方案及测试结果 S32K324的M…

Pytorch猴痘病识别

Pytorch猴痘病识别 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 电脑系统&#xff1a;Windows11 显卡型号&#xff1a;NVIDIA Quadro P620 语言环境&#xff1a;python 3.9.7 编译器&#xff1a;jupyte…

网络安全渗透实际案例

目录 案例场景案例目标和工具 案例操作步骤Step 1&#xff1a;信息收集与识别**结果分析** Step 2&#xff1a;漏洞扫描**预期结果** Step 3&#xff1a;漏洞利用与权限验证Step 4&#xff1a;后渗透测试Step 5&#xff1a;报告生成和修复建议**修复建议** 案例总结 下面是一个…

快消零售行业的培训创新:构建在线培训知识库

在快速消费品&#xff08;FMCG&#xff09;行业中&#xff0c;员工的培训和发展对于保持竞争力至关重要。随着电子商务的兴起和消费者行为的变化&#xff0c;快消零售行业需要不断适应新的市场趋势。在线培训知识库作为一种有效的培训工具&#xff0c;可以帮助企业提升员工技能…

软考(中级-软件设计师)计算机网络篇(1101)

第五章&#xff1a;计算机网络基础 **考纲要求**根据开始大纲中相应的考核要求&#xff0c;要求考生掌握一下方面的内容&#xff1a; 1、计算机网络基础知识 网络体系结构传输介质、传输技术、传输方法、传输控制常用网络设备和各类通信设备的特点Client-Server结构、Browser…

【毫米波雷达(四)】车载毫米波雷达下线EOL标定流程

汽车控制器下线EOL标定流程 一、概述二、标定的目的三、雷达标定的要求1、车辆的要求2、标定环境要求四、以软件的角度分析前雷达的EOL标定 一、概述 由于雷达的安装误差会影响雷达对目标位置的检测&#xff0c;导致报警及功能性能下降。因此雷达进行预安装后必须进行角度标定…

免费插件集-illustrator插件-Ai插件-闭合开放路径

文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.总结 1.介绍 本文介绍一款免费插件&#xff0c;加强illustrator使用人员工作效率&#xff0c;实现图形编辑中闭合开放路径。首先从下载网址下载这款插件https://download.csdn.net/download/m0_67316550/8789…

LDA 线性分类

线性判别分析是一种经典的线性分类方法&#xff0c;将高维空间投射到低维空间&#xff0c;如下图。 LDA 的目标就是简单累内距离变小&#xff0c;把类间的距离变大&#xff0c;这样就可以把相似的数据聚集在一起。 u1 和 u2 类间距离&#xff0c;S1、S2 为类内数据点之间的距…

面试必会50题

基础篇 01 和 equals 的区别是什么 : 可以比较基本数据类型也可以比较引用数据类型 , 比较基本数据类型是比较值是否相等, 比较引用数据类型是比较引用地址是否相等 (基本数 据类型 比较的是值&#xff0c;引用数据类型 比较的是内存地址) equals() : 一般用于对象的比较…

Python 工具库每日推荐 【Sphinx】

文章目录 引言文档工具的重要性今日推荐:Sphinx 文档生成工具主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:为 Python 项目生成 API 文档案例分析高级特性自定义主题国际化支持扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 TypeScript …

Pinctrl子系统中Pincontroller构造过程驱动分析:imx_pinctrl_soc_info结构体

往期内容 本专栏往期内容&#xff1a; Pinctrl子系统和其主要结构体引入Pinctrl子系统pinctrl_desc结构体进一步介绍Pinctrl子系统中client端设备树相关数据结构介绍和解析 input子系统专栏&#xff1a; 专栏地址&#xff1a;input子系统input角度&#xff1a;I2C触摸屏驱动分析…

第十五章 Vue工程化开发及Vue CLI脚手架

目录 一、引言 二、Vue CLI 基本介绍 三、安装Vue CLI 3.1. 安装npm和yarn 3.2. 安装Vue CLI 3.3. 查看 Vue 版本 四、创建启动工程 4.1. 创建项目架子 4.2. 启动工程 五、脚手架目录文件介绍 六、核心文件讲解 6.1. index.html 6.2. main.js 6.3. App.vue 一、…

Rust 力扣 - 2841. 几乎唯一子数组的最大和

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们遍历长度为k的窗口&#xff0c;用一个哈希表记录窗口内的所有元素&#xff08;用来对窗口内元素去重&#xff09;&#xff0c;我们取哈希表中元素数量大于等于m的窗口总和的最大值 题解代码 use std::coll…

Python数据分析案例61——信贷风控评分卡模型(A卡)(scorecardpy 全面解析)

案例背景 虽然在效果上&#xff0c;传统的逻辑回归模型通常不如现代的机器学习模型&#xff0c;但在风控领域&#xff0c;解释性至关重要。逻辑回归的解释性是这些“黑箱”模型所无法比拟的&#xff0c;因此&#xff0c;研究传统的评分卡模型依然是有意义的。 传统的评分卡模型…

免费送源码:Java+Springboot+MySQL Springboot酒店客房管理系统的设计与实现 计算机毕业设计原创定制

摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对酒店客房管理等问题&#xff0c;对酒店客房…

力扣每日一题 超级饮料的最大强化能量 动态规划(dp)

来自未来的体育科学家给你两个整数数组 energyDrinkA 和 energyDrinkB&#xff0c;数组长度都等于 n。这两个数组分别代表 A、B 两种不同能量饮料每小时所能提供的强化能量。 你需要每小时饮用一种能量饮料来 最大化 你的总强化能量。然而&#xff0c;如果从一种能量饮料切换到…

Linux高阶——1027—守护进程

1、守护进程的基本流程 1、父进程创建子进程&#xff0c;父进程退出 守护进程是孤儿进程&#xff0c;但是是工程师人为创建的孤儿进程&#xff0c;低开销模式运行&#xff0c;对系统没有压力 2、子进程&#xff08;守护进程&#xff09;脱离控制终端&#xff0c;创建新会话 …

抗疫物资管理:SpringBoot技术应用案例

目 录 摘 要 1 前 言 2 第1章 概述 2 1.1 研究背景 3 1.2 研究目的 3 1.3 研究内容 4 第二章 开发技术介绍 5 2.1相关技术 5 2.2 Java技术 6 2.3 MySQL数据库 6 2.4 Tomcat介绍 7 2.5 Spring Boot框架 8 第三章 系统分析 9 3.1 可行性分析 9 3.1.1 技术可行性 9 3.1.2 经济可行…