高并发处理 --- 超卖问题+一人一单解决方案

在高并发场景下,超卖一人一单是两个典型的并发问题。为了解决这两个问题,我们可以使用乐观锁(CAS)和悲观锁,这两者分别有不同的实现方式和适用场景。下面我们详细介绍如何通过 乐观锁(CAS)悲观锁 来解决这两个问题。


假设我们有一张库存表 seckill_voucher,其中包含字段:

  • voucher_id: 优惠券ID
  • stock: 库存数量
CREATE TABLE seckill_voucher (voucher_id BIGINT PRIMARY KEY,stock INT,
);

这里我们使用自定义的全局唯一ID生成器来创建缓存中的key:

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;@Component
public class RedisIdWorker {// 开始时间戳private static final long BEGIN_TIMESTAMP = 1735689600L; // 2025.01.01.00.00.00// 序列号的位数private static final long COUNT_BITS = 32;private final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 全局唯一ID生成器:(long)符号位+时间戳+序列号public long nextId(String keyPrefix) {// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC); // 获取当前秒数long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列号// 2.1 获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2 自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}//    public static void main(String[] args) {
//        LocalDateTime localDateTime = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
//        long second = localDateTime.toEpochSecond(ZoneOffset.UTC);
//        System.out.println(second); // 1735689600
//    }
}

我们正常的一个用户秒杀代码操作如下:

查询优惠卷信息 -> 判断秒杀是否开始 -> 判断秒杀是否结束 -> 判断秒杀卷是否充足 -> 扣减库存 -> 创建订单

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}return createVoucherOrder(voucherId); }private Result createVoucherOrder(Long voucherId) {//5,扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idLong userInId = UserHolder.getUser().getId();voucherOrder.setUserId(userInId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}
}

但是对于上面代码存在很大的问题:

如上图,假设商品剩余数量为1,那么对于多个线程同时对仅剩一件进行竞争,会导致线程安全问题,这个时候我们就会使用锁来解决,而锁主要分为两大类:悲观锁与乐观锁。

那么接下来我们使用乐观锁解决上述问题:

在大多数情况下,数据基本不会发生冲突,因此在更新操作前不加锁,而是在提交时验证数据是否有冲突。如果数据被其他线程更新修改则操作失败,调用方可以选择重试或返回错误。乐观锁的常见实现方式是使用 CAS(Compare-And-Swap)

乐观锁的关键在于判断之前查询得到的数据是否有被修改过。

高并发支持:CAS操作不需要加锁,因此适合高并发场景,减少了锁竞争,提高了性能。

无死锁:与悲观锁相比,CAS不会发生死锁问题,因为它不需要锁住资源。

通过上面对于问题以及CAS解决方案的分析介绍,我们在扣减库存之前,需要对stock库存量进行判断,是否为查询到的值,如果为查询到的值,那么就能证明在此期间未曾被修改过。

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}return createVoucherOrder(voucherId);}private Result createVoucherOrder(Long voucherId) {// 一人一单Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count();// 判断是否存在if(count > 0){// 用户已经购买过return Result.fail("用户已经购买过");}//5,扣减库存
//        boolean success = seckillVoucherService.update()
//                .setSql("stock= stock -1")
//                .eq("voucher_id", voucherId).update();// 乐观锁(操作前先判断是否有更新操作) -> CAS方法(局限:多个线程只能卖出一次)boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherId).eq("stock",voucher.getStock()) // where id = ? and stock = ?.update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单// ...}
}

但是注意,虽然我们对线程安全问题进行了处理,但是这样处理后的结果是对数据库产生的压力增大,并且假设有一百个线程同时对库存量为100的商品进行操作,那么大概率只能有一个线程能够成功(因为同一时间只能有一个被修改),这样操作一波后库存量变为99,成功率极低,并且对于业务处理很不友好,那么这个时候我们就需要将数据库操作语句更改为stock > 0,就可以解决上述问题,只对于商品的临界数进行限制就可以很好的处理该问题。

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}return createVoucherOrder(voucherId);}private Result createVoucherOrder(Long voucherId) {// 一人一单Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count();// 判断是否存在if(count > 0){// 用户已经购买过return Result.fail("用户已经购买过");}//5,扣减库存// 乐观锁(操作前先判断是否有更新操作) -> CAS方法(局限:多个线程只能卖出一次)
//        boolean success = seckillVoucherService.update()
//                .setSql("stock= stock -1") // set stock = stock - 1
//                .eq("voucher_id", voucherId).eq("stock",voucher.getStock()) // where id = ? and stock = ?
//                .update();// CAS改进:将库存判断改成stock > 0以此来提高性能boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherId).eq("stock",0) // where id = ? and stock > 0.update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单// ...}
}

通过上面方法我们成功解决了超卖场景的高并发导致的线程安全问题,那么接下来我们就要解决一人一单的问题了。


对于一人一单问题要求一个用户只能购买一次某个优惠券。在上述代码中,我们可以通过查询数据库中是否已经有用户购买的记录来避免一个用户重复购买:

查询优惠卷信息 -> 判断秒杀是否开始 -> 判断秒杀是否结束 -> 判断秒杀卷是否充足 ->  判断是否满足一人一单 -> 扣减库存 -> 创建订单

如果同一用户已经存在购买记录,则不能再次购买,避免了一人多单问题,并且这个逻辑也与 乐观锁(CAS) 配合使用,确保用户只能成功创建一次订单。

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// ...return createVoucherOrder(voucherId);}}private Result createVoucherOrder(Long voucherId) {// 一人一单处理代码Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count();// 判断是否存在if(count > 0){// 用户已经购买过return Result.fail("用户已经购买过");}//5,扣减库存// CAS改进:将库存判断改成stock > 0以此来提高性能boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherId).eq("stock",0) // where id = ? and stock > 0.update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单// ...}
}

那么同样的道理,在高并发的场景下,假设有100个用户都没有购买过,那么 if 语句就不会拦截,虽然通过 eq("stock", 0) 确保只有当库存大于0时才会成功扣减库存,但是在高并发情况下,仍然可能会发生以下几种情况:

  • 重复购买:多个用户几乎同时请求购买同一张优惠券,虽然你检查了库存并执行了扣减操作,但在高并发时,库存检查扣减操作之间的时间差可能非常小。如果两个请求几乎同时到达,可能会先检查库存是否大于0然后再进行扣减,而两者在检查时库存都大于0,导致两次扣减操作都成功,从而导致超卖。

  • 一人多单:虽然你通过查询订单判断用户是否已经购买过,但在高并发的情况下,如果两个请求几乎同时发起,在检查订单时两个请求都没有找到订单(因为数据库操作存在延迟),导致两者都认为用户没有购买过,从而都创建了订单。

这些步骤虽然在逻辑上是连贯的,但它们并不是原子性的,尤其是在高并发时。即使有乐观锁来处理库存问题,但对于 一人一单 的逻辑,仍然可能存在数据库查询和插入的竞争条件。两个请求同时检测到用户没有购买过,分别尝试创建订单,这时即便库存只有一张,还是可能出现两个订单。

那么这个时候我们就需要加悲观锁来将下面步骤锁上,以此来同时处理:

synchronized 判断是否满足一人一单 -> 扣减库存 -> 创建订单 }

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// ...return createVoucherOrder(voucherId);}@Transactionalpublic synchronized Result createVoucherOrder(Long voucherId) {// 一人一单Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count();// 判断是否存在if(count > 0){// 用户已经购买过return Result.fail("用户已经购买过");}//5,扣减库存// CAS改进:将库存判断改成stock > 0以此来提高性能boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherId).eq("stock",0) // where id = ? and stock > 0.update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单// ...}
}

这样操作后,每个用户的订单创建请求都会被顺序处理,在同一时刻只能有一个线程可执行该方法,其他线程会被阻塞。但这个方案的缺点是锁的粒度过大,可能会导致性能瓶颈,特别是在高并发的情况下。

这种情况我们可以通过 synchronized (userId.toString().intern()) 锁定每个用户 ID。这个锁的粒度是针对单个用户的。当多个线程尝试处理同一用户的订单时,它们会被串行化处理,其他用户的操作不会受到影响。如果多个线程操作不同的用户,那么它们的操作仍然可以并发进行。因为我们只需要限制对于一个用户的多个线程不能同时进行扣减库存以及创建订单操作就可以了。

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 获取代理对象  (需要引入aspectjweaver依赖并在实现类加入@EnableAspectJAutoProxy(exposeProxy = true))return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 一人一单Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){// 查询订单int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count();// 判断是否存在if(count > 0){// 用户已经购买过return Result.fail("用户已经购买过");}//5,扣减库存
//        boolean success = seckillVoucherService.update()
//                .setSql("stock= stock -1")
//                .eq("voucher_id", voucherId).update();// 乐观锁(操作前先判断是否有更新操作) -> CAS方法(局限:多个线程只能卖出一次)
//        boolean success = seckillVoucherService.update()
//                .setSql("stock= stock -1") // set stock = stock - 1
//                .eq("voucher_id", voucherId).eq("stock",voucher.getStock()) // where id = ? and stock = ?
//                .update();// CAS改进:将库存判断改成stock > 0以此来提高性能boolean success = seckillVoucherService.update().setSql("stock= stock -1") // set stock = stock - 1.eq("voucher_id", voucherId).eq("stock",0) // where id = ? and stock > 0.update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idLong userInId = UserHolder.getUser().getId();voucherOrder.setUserId(userInId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);}}
}

两种加锁位置的不同,为什么加在方法内部较好?


① 对于事务方面,两者都使用了 @Transactional 注解来确保事务的一致性和原子性。事务的本质是保证操作的 一致性,即在整个操作过程中,如果出现异常可以回滚。因此,@Transactional 确保了 订单创建、库存扣减等操作的事务性,即使使用了锁,也不会改变操作的原子性。然而,方法级锁(第二种方式)可能会引发 长时间的锁竞争,特别是在库存扣减、订单查询等操作较多时,可能会导致 事务执行时间过长,这会影响性能。

② 对于细粒度控制,在第二种方式中,锁的粒度更细,锁定的范围仅限于特定的用户。不同用户的请求可以并发处理,提高了系统的吞吐量。而在第一种方式中,整个方法都被锁住了,所有请求都必须排队等待,影响了系统的并发能力。

锁定用户ID(第二种方式)则避免了不同用户之间的锁竞争,只会针对同一个用户的请求加锁。


为什么要这么写userId.toString().intern()


① userId.toString() 会将 Long 类型的 userId 转换为字符串。这样做是因为在 Java 中,synchronized 关键字要求锁住的对象必须是一个 对象(Object),而 userIdLong 类型的原始数据类型(primitive),不能直接用于 synchronized 的锁定。所以我们需要先将它转换为 String 类型。另外一个原因就是因为对于每一个请求接收到的线程内的user都是全新的user对象,是变化的,而值是固定的,所以我们给他转换为String类型,但是toString()方法的底层最后是返回 new 一个String对象,而这样肯定也不可以作为锁的条件,所以我们要再字符串池内拿到锁的实例,因为实例是唯一的,就需要调用下面的方法intern()

String lock1 = userId.toString(); // 新创建一个字符串对象
String lock2 = userId.toString(); // 再次创建一个新的字符串对象

② intern() 方法的作用是返回该字符串在 JVM 字符串池中的唯一引用。JVM 会为所有常量字符串(如 "abc")以及通过 intern() 方法处理过的字符串,保证它们在内存中只有一份唯一的实例。当我们调用 intern() 时,JVM 会检查字符串池中是否已经存在这个字符串。如果存在,就返回这个池中的实例;如果不存在,则将这个字符串加入池中。

String lock1 = userId.toString().intern(); // 锁定唯一的字符串对象
String lock2 = userId.toString().intern(); // 锁定同一个字符串对象

③ 在这里,userId.toString().intern() 的目的就是利用 字符串池 来确保针对相同 userId 的锁是 唯一的。通过这种方式,不同线程针对同一个 userId 发起请求时,它们会共享同一个锁对象。并且通过使用字符串池,可以减少内存占用,同时避免每次都为每个 userId 创建新的锁对象,这样在并发的情况下,加锁和释放锁的效率会更高

但是这样之后还有一个问题,我们在处理完这三部操作后,锁就会打开,这个时候其他的线程就可以进来,但是因为事务是整个方法,此时一但事务没有提交结束,其他线程直接进入锁,就会造成线程安全问题,所以我们需要将这个锁的范围扩大:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// ...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){return this.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {// ...}
}

但是我们只是对 createVoucher 函数加入事务处理而并没有给 seckillVoucher 函数加入,所以 seckillVoucher 函数调用 createVoucher 函数是使用 this 进行调用,而这个 this 仅是当前类对象,而不是该类的代理对象,因为事务如果想要生效,Spring 是对当前类做一个动态代理进而拿到其代理对象,随后使用其代理对象来进行事务处理,所以这个时候就需要我们拿到事务的代理对象:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// ...Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){// 获取当前类的代理对象 // 需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {// ...}
}

通过上面所讲述使用悲观锁与乐观锁可以很好的解决单机情况下的一人一单问题,但是在集群模式就不行了,这是为什么呢?

首先我们可以模拟一下集群模式,打开idea,按住ALT+8打开服务框,点击+号,选中Springboot添加就能够出来端口:

之后点击该项目按住Ctrl+D:

因为我的是2024年的idea,需要点击修改选项,然后点击覆盖配置属性:

 

那么这样就可以出现集群了:

 

随后运行我们的代码可以发现,当我们同时访问的一个nginx代理的8080端口后,我们可以发现下面的锁是产生了问题:

synchronized (userId.toString().intern()){// 获取当前类的代理对象 // 需要引入aspectjweaver依赖,并且在实现类加入@EnableAspectJAutoProxy(exposeProxy = true)以此来暴露代理对象IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId);
}

在单体项目的情况,我们只是对一个JVM虚拟机内的锁监视器进行操作,而在多集群模式下就会有多个所监视器,这样的锁就无法正确的使用了,如下图:

那么我们如何让锁在两个甚至多个集群下进行使用来达到我们能够锁住呢?

这个时候我们就需要使用一个能够跨JVM也就是跨进程的锁 --- redis分布式锁。


有关redis分布式锁的内容请看下篇博客: 

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

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

相关文章

【2024年华为OD机试】(C卷,100分)- 约瑟夫问题 (JavaScriptJava PythonC/C++)

一、问题描述 题目描述 输入一个由随机数组成的数列&#xff08;数列中每个数均是大于 0 的整数&#xff0c;长度已知&#xff09;&#xff0c;和初始计数值 m。 从数列首位置开始计数&#xff0c;计数到 m 后&#xff0c;将数列该位置数值替换计数值 m&#xff0c;并将数列…

浅谈APP之历史股票通过echarts绘图

浅谈APP之历史股票通过echarts绘图 需求描述 今天我们需要做一个简单的历史股票收盘价格通过echarts进行绘图&#xff0c;效果如下&#xff1a; 业务实现 代码框架 代码框架如下&#xff1a; . 依赖包下载 我们通过网站下载自己需要的涉及的图标&#xff0c;勾选之后进…

【0x0012】HCI_Delete_Stored_Link_Key命令详解

目录 一、命令参数 二、命令格式及参数 2.1. HCI_Delete_Stored_Link_Key 命令格式 2.2. BD_ADDR 2.3. Delete_All 三、生成事件及参数 3.1. HCI_Command_Complete事件 3.2. Status 3.3. Num_Keys_Deleted 四、命令执行流程 4.1. 命令发送阶段 4.2. 控制器处理阶段…

提示词的艺术 ---- AI Prompt 进阶(提示词框架)

提示词的艺术 ---- AI Prompt 进阶&#xff08;提示词框架&#xff09; 写在前面 上周发布了一篇《提示词的艺术----AI Prompt撰写指南》&#xff0c;旨在帮助读者理解提示词的作用&#xff0c;以及简单的提示词撰写指南。本篇作为进阶内容&#xff0c;将给出常用的提示词框架…

javaSE.类的继承

在定义不同类的时候,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有成员. extents 没有可用的无形参构造方法 被构造方法覆盖了 super 需要调用父类的构造方法 super必须是构造主体的第一条语…

统计文本文件中单词频率的 Swift 与 Bash 实现详解

网罗开发 &#xff08;小红书、快手、视频号同名&#xff09; 大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等…

qt QUrl详解

1、概述 QUrl是Qt框架中用于处理URL&#xff08;统一资源定位符&#xff09;的类&#xff0c;它提供了构建、解析、编码、解码和处理URL的功能。QUrl支持多种协议&#xff0c;如HTTP、HTTPS、FTP以及文件URL等&#xff0c;并能处理URL的各个组成部分&#xff0c;如协议、主机、…

c++----------------------多态

1.多态 1.1多态的概念 多态(polymorphism)的概念&#xff1a;通俗来说&#xff0c;就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态)&#xff0c;这⾥我们重点讲运⾏时多态&#xff0c;编译时多态(静态多态)和运⾏时多态(动态多态)。编译时 多态(静态多态)…

javaSE.类与对象

类与对象 人类&#xff0c;鸟类&#xff0c;鱼类... 例如人&#xff0c;具有不同性格&#xff0c;但根本上都是人。 对象是某一类事物实际存在的每个个体&#xff08;实例&#xff09;例如&#xff1a;雷军 A:谁拿走了我的手机&#xff1f; B:是个人&#xff08;类&#xff0…

Windows cmd常用命令

文章目录 Windows cmd常用命令一、引言二、文件和目录操作1、查看和切换目录2、文件和目录的创建与删除 三、系统信息与网络配置1、系统信息2、网络配置 四、使用示例五、总结 Windows cmd常用命令 一、引言 Windows 命令提示符&#xff08;cmd&#xff09;是一个强大的工具&a…

保健食品注册数据库<一键查询保健食品信息>

在保健品市场竞争激烈的情况下&#xff0c;企业要如何保障产品合规、信息公开&#xff0c;并且能够迅速应对市场变化呢&#xff1f;查询保健食品注册信息是关键环节。 当下&#xff0c;查询保健食品注册信息主要有两种途径&#xff1a;一是利用国家保健食品注册数据库进行查询…

无所不搜,吾爱制造

吾爱论坛作为众多软件资源爱好者的宝藏之地&#xff0c;汇聚了许多优秀的软件作品&#xff0c;堪称软件界的“福地”。许多技术大佬在这里分享自己的创作。 而今天要介绍的&#xff0c;正是吾爱作者“buyaobushuo”自制的多功能娱乐软件——太极。这款软件基于flet开发&#x…

【C++】详细讲解继承(下)

本篇来继续说说继承。上篇可移步至【C】详细讲解继承&#xff08;上&#xff09; 1.继承与友元 友元关系不能继承 &#xff0c;也就是说基类友元不能访问派⽣类私有和保护成员。 class Student;//前置声明class Same //基类 { public:friend void Fun(const Same& p, con…

【二叉树】4. 判断一颗二叉树是否是平衡二叉树。5. 对称二叉树。6. 二叉树的构建及遍历 7. 二叉树的分层遍历 。

判断一颗二叉树是否是平衡二叉树。OJ链接 可以在求树高度的过程中判断树是否平衡 对称二叉树。OJ链接 二叉树的构建及遍历。OJ链接 注意&#xff1a;public static int i最好把static去掉 否则当有多个测试用例时 i无法重新为0二叉树的分层遍历 。OJ链接 但此题要求返回List…

代码随想录刷题day14(2)|(链表篇)02.07. 链表相交(疑点)

目录 一、链表理论基础 二、链表相交求解思路 三、相关算法题目 四、疑点 一、链表理论基础 代码随想录 二、链表相交求解思路 链表相交时&#xff0c;是结点的位置&#xff0c;也就是指针相同&#xff0c;不是结点的数值相同&#xff1b; 思路&#xff1a;定义两个指针…

IDE提示:因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170

问题情况 不知道为什么我的IDE终端运行命令的时候总提示以下内容&#xff1a; Import-Module : 无法加载文件 D:\Anaconda3\shell\condabin\Conda.psm1&#xff0c;因为在此系统上禁止运行脚本。有关详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID1351…

Android中Service在新进程中的启动流程3

目录 1、AMS调用客户端onCreate前的准备工作 2、AMS调用客户端onCreate方法 3、AMS调用客户端的onBind方法 4、AMS调用客户端onStart前的准备 5、AMS调用客户端onStart方法 还是先放上Service启动流程概览图&#xff0c;如下&#xff1a; 上一篇文章&#xff0c; 我们分析…

MVCC底层原理实现

MVCC的实现原理 了解实现原理之前&#xff0c;先理解下面几个组件的内容 1、 当前读和快照读 先普及一下什么是当前读和快照读。 当前读&#xff1a;读取数据的最新版本&#xff0c;并对数据进行加锁。 例如&#xff1a;insert、update、delete、select for update、 sele…

基于Springboot + vue实现的在线装修管理系统

“前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff1a;人工智能学习网站” &#x1f496;学习知识需费心&#xff0c; &#x1f4d5;整理归纳更费神。 &#x1f389;源码免费人人喜…

【多视图学习】显式视图-标签问题:多视图聚类的多方面互补性研究

Explicit View-labels Matter:A Multifacet Complementarity Study of Multi-view Clustering TPAMI 2024 论文链接 代码链接 0.论文摘要 摘要-一致性和互补性是促进多视图聚类&#xff08;MVC&#xff09;的两个关键因素。最近&#xff0c;随着流行的对比学习的引入&#…