Redis企业开发实战(三)——点评项目之优惠券秒杀

目录

一、全局唯一ID 

(一)概述 

(二)全局ID生成器 

(三)全局唯一ID生成策略 

1. UUID (Universally Unique Identifier)

2. 雪花算法(Snowflake)

3. 数据库自增

4. Redis INCR/INCRBY

5.总结

(四)Redis实现全局唯一ID

1.工具类

2.测试类

3.关于countdownlatch 

二、实现优惠券秒杀下单

三、超卖问题

(一)超卖问题出现的原因

(二)乐观锁解决超卖问题 

1.悲观锁

2.乐观锁

3.乐观锁的CAS

4.CAS的自旋 

4.1如何缓解自旋压力

(三)超卖问题总结

四、超领问题(一人一单)

(一)需求说明

(二)悲观锁解决单机情况的超领问题

(三)特别说明!!!

(四)集群模式下的超领问题

1.模拟集群模式

2.断点调试 

3.运行结果 

4.结果分析——锁监视器


一、全局唯一ID 

(一)概述 

        每个店铺都可以发布优惠券:

        当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

        场景分析一:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

        场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

(二)全局ID生成器 

        全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:唯一性、高可用、高性能、递增性、安全性。 

        为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的类型:Long类型,8个字节,64位

ID的组成部分:

  • u符号位:1bit,永远为0,表示一个正数
  • u时间戳:31bit,以秒为单位,可以使用69年
  • u序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

(三)全局唯一ID生成策略 

常见的全局唯一ID生成策略及其优缺点和适用场景:

1. UUID (Universally Unique Identifier)

实现方式

        UUID通常是一个128位的值,可以通过多种算法生成,如基于时间戳、随机数或MAC地址等。

优点

        简单易用:无需额外的基础设施支持。

        高可用性:由于其生成机制,几乎可以保证全球唯一性。

        无中心化管理:可以在任何地方独立生成,非常适合分布式环境。

缺点

        长度较长:标准的UUID为128位,占用较多存储空间。

        性能问题:对于某些高性能要求的应用场景,生成速度可能成为瓶颈。

        不可排序:UUID不具备自然的时间顺序,不利于按时间排序查询。

使用场景

        适合于对唯一性有严格要求但对排序和性能要求不高的场景,例如日志记录、会话标识等。

2. 雪花算法(Snowflake)

实现方式

        Twitter Snowflake是一种分布式ID生成算法,生成64位的整型ID,包含时间戳、机器ID、数据中心ID和序列号。

优点

        高效:能够快速生成大量唯一的ID。

        有序性:生成的ID按时间顺序递增,有利于数据库索引优化。

        可扩展性:支持分布式部署,每个节点都可以独立生成ID。

缺点

        依赖时钟同步:如果服务器之间的时间不同步,可能会导致ID冲突。

        复杂度较高:需要考虑机器ID分配、数据中心配置等问题。

使用场景

        适合于高并发环境下需要快速生成有序ID的场景,如订单编号生成、用户ID生成等。

3. 数据库自增

实现方式

        利用关系型数据库提供的自增字段功能来生成ID。

优点

        简单直接:实现起来非常简单,直接利用数据库特性。
        天然有序:自增ID天然具有顺序性,便于后续处理。

缺点

        单点故障风险:如果使用单一数据库实例,则存在单点故障的风险。
        不适合分布式系统:在分布式环境下,难以保证全局唯一性且性能受限。

使用场景

        适合于小型应用或不需要高度分布式的场景。对于需要极高可靠性的大型分布式系统,通常需要结合其他技术(如分段分配)使用。

4. Redis INCR/INCRBY

实现方式

        通过Redis的INCR或INCRBY命令来生成自增ID。

优点

        高性能:Redis作为内存数据库,操作速度快。

        原子性:INCR操作是原子性的,确保ID唯一性。

        易于扩展:可以方便地与现有Redis集群集成。

缺点

        有限范围:虽然Redis支持64位整数,但对于某些超大规模的应用仍可能不足。
        外部依赖:增加了对Redis服务的依赖,若Redis出现故障会影响ID生成。

使用场景

        适用于中等到大规模应用中需要高效生成唯一ID的情况,尤其是那些已经使用了Redis作为缓存或其他用途的项目。

5.总结

  1. UUID:适合去中心化的应用,对唯一性和简易性有较高要求的场合。
  2. 雪花算法:适用于高并发、分布式环境下的有序ID生成需求。
  3. 数据库自增:简单直接,但更适合于非分布式或者规模较小的应用。
  4. Redis INCR:提供了高效的ID生成方案,并且容易与现有的Redis架构集成,适合中到大规模应用。

        选择哪种方法取决于具体的应用需求、系统架构以及性能要求等因素。在实际开发过程中,也可以根据具体情况混合使用上述方法以达到最佳效果。

(四)Redis实现全局唯一ID

Redis自增ID策略:

  • 每天一个key,方便统计订单量
  • ID构造是时间戳+计数器

1.工具类

@Component
@Slf4j
public class RedisIdWorker {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 初始时间戳
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    // 位数
    private static final int COUNT_BITS = 32;

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        // 获取当前时间戳,单位为秒,使用当前时间戳减去初始时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowEpochSecond = now.toEpochSecond(ZoneOffset.UTC);
        long currentTimeStamp = nowEpochSecond - BEGIN_TIMESTAMP;
        // 2.生成序列号,使用redis的自增长
        // 2.1 获取当前日期,精确到天,好处是:避免超过32位的上限,和方便按照日期查询
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 这里不会存在空指针问题,
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.使用位运算拼接并返回
        /**
         * 或运算:
         * 将两个数转为二进制,对于每个位,如果两个相应的位有一个为 1,则结果位为 1;否则为 0
         * 这里左移32位后,剩余的32位全部为0,使用或运算,存放的就全部是序列号了
         */
        return currentTimeStamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long epochSecond = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(epochSecond); // 1640995200
    }
}

2.测试类

@SpringBootTest
class HmDianPingApplicationTests {
    @Resource
    private CacheClient cacheClient;

    @Resource
    private RedisIdWorker redisIdWorker;

    // 线程池
    private ExecutorService es = Executors.newFixedThreadPool(500);

    @Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);
        Runnable task = () -> {
            // 生成100个id
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        long start = System.currentTimeMillis();
        // 将任务提交300次,会生成30000个id
        for (int i = 0; i < 300; i++) {
            es.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time=" + (end - start)); // time=1978毫秒
    }
}

3.关于countdownlatch 

        countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题

我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch。

CountDownLatch 中有两个最重要的方法

1、countDown

2、await

        await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch  内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch   维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。 

运行结果: 

 

二、实现优惠券秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

 

@Override
public Result seckillVoucher(Long voucherId) {
    // 查询优惠券是否存在
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    // 查询秒杀是否开始
    LocalDateTime beginTime = seckillVoucher.getBeginTime();
    if (beginTime.isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始");
    }
    // 查询秒杀是否结束
    LocalDateTime endTime = seckillVoucher.getEndTime();
    if (endTime.isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束");
    }
    // 判断库存是否充足
    Integer stock = seckillVoucher.getStock();
    if (stock < 1) {
        return Result.fail("库存不足");
    }
    // 扣减库存
    seckillVoucher.setStock(stock - 1);
    boolean success = seckillVoucherService.updateById(seckillVoucher);
    if (!success){
        return Result.fail("库存不足");
    }
    // 创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    // 订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 用户id
    voucherOrder.setUserId(UserHolder.getUser().getId());
    // 代金券id
    voucherOrder.setVoucherId(voucherId);
    // 创建订单详情
    save(voucherOrder);
    // 返回订单id
    return Result.ok(orderId);
}

三、超卖问题

(一)超卖问题出现的原因

        假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。

(二)乐观锁解决超卖问题 

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

1.悲观锁

        认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如Synchronized、Lock都属于悲观锁。

2.乐观锁

        认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时,去判断有没有其他线程对数据做了修改。

  • 如果没有修改,则认为是安全的,才会进行数据更新;
  • 如果已经被其它线程修改,说明发生了线程安全问题,此时可以重试或者抛异常。

        乐观锁会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功,这套机制的核心逻辑在于,如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas 

 

3.乐观锁的CAS

        CAS(Compare-And-Swap)是一个底层的原子操作,它可以被用来实现乐观锁。CAS操作能够确保只有当预期值与内存中的当前值相等时,才会进行更新,否则更新失败。这种特性非常适合于实现乐观锁,特别是在无锁编程中。

        在本项目中,CAS不设置版本号,因为版本号的操作和库存的操作是一样的,所以使用stock库存代替版本号。

 

boolean success = seckillVoucherService.update()
        .setSql("stock = stock - 1") // set stock = stock - 1
        .eq("voucher_id", voucherId)
        .gt("stock", 0) // where voucher_id = ? and stock > 0
        .update();
if (!success) {
    return Result.fail("库存不足");
}

4.CAS的自旋 

        “CAS操作本质上是一个原子操作,它尝试比较内存位置的当前值与预期值是否相同,如果相同,则更新为新值;否则,不进行任何修改,并返回失败。

        当多个线程同时试图对同一变量执行CAS操作时,只有一个线程能够成功更新该变量,其余线程将收到失败的结果。为了实现乐观锁或其他无锁算法,这些失败的线程通常会进入一个循环,反复尝试直到成功更新为止。这个过程被称为“自旋”。

4.1如何缓解自旋压力
  1. 退避策略:可以在每次CAS失败后引入短暂的休眠或等待时间(如使用Thread.yield()Thread.sleep(n)),以减少CPU的占用率。这种方式可以降低自旋频率,但也会增加总的延迟。

  2. 限制重试次数:设定一个最大重试次数,超过该次数则采取其他措施,比如回退并重新排队或者抛出异常让上层逻辑处理。

  3. 结合锁机制:在极端高争用的情况下,考虑切换回传统的锁机制(如互斥锁),尽管这会牺牲一些并发性,但可以避免过度的自旋压力。

  4. 优化数据结构设计:通过优化共享数据结构的设计来减少热点争用点,例如分片存储、局部化访问等方法,可以有效降低CAS操作的竞争程度。

  5. 使用更高级别的同步工具:现代编程语言和框架提供了许多高级别的同步原语(如读写锁、信号量等),它们能够在不同场景下提供更好的性能和灵活性。

(三)超卖问题总结

1.悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般

2.乐观锁:不加锁,在更新时判断是否有其它线程在修改

  • 优点:性能好
  • 缺点:存在成功率低的问题

四、超领问题(一人一单)

(一)需求说明

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单。

        乐观锁是在更新数据的时候使用的,而这里的领取优惠券是插入数据,每个用户领取一个优惠券会新增一条优惠券订单,因此需要使用悲观锁来解决。从查询订单,到判断订单,最后到新增订单都要放在锁里面。

(二)悲观锁解决单机情况的超领问题

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查询优惠券是否存在
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        if (seckillVoucher == null) {
            return Result.fail("优惠券不存在");
        }
        // 查询秒杀是否开始
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        if (beginTime.isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始");
        }
        // 查询秒杀是否结束
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if (endTime.isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束");
        }

        // 判断库存是否充足
        Integer stock = seckillVoucher.getStock();
        if (stock < 1) {
            return Result.fail("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 获取和事务有关的代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            // 返回订单id
            return proxy.createVoucherOrder(voucherId);
        }
    }
    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 同一个用户加锁
        Long userId = UserHolder.getUser().getId();
        // 一人一单
        long count = query()
                .eq("user_id", userId)
                .eq("voucher_id", voucherId)
                .count();
        if (count > 0) {
            return Result.fail("用户已经购买过一次了");
        }

        // 扣减库存
//        seckillVoucher.setStock(stock - 1);
//        boolean success = seckillVoucherService.updateById(seckillVoucher);
        /**
         * 为什么有两个 update()
         * 第一个 update() 实际上是 MyBatis-Plus 提供的一个便捷入口,用来开始构建更新操作的链式调用。
         * 第二个 update() 是真正执行数据库更新的方法,它基于之前通过链式调用定义的所有条件和设置来进行更新。
         */
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId)
                .gt("stock", 0) // where voucher_id = ? and stock > 0
                .update();
        if (!success) {
            return Result.fail("库存不足");
        }

        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 用户id
        voucherOrder.setUserId(userId);
        // 代金券id
        voucherOrder.setVoucherId(voucherId);
        // 创建订单详情
        save(voucherOrder);
        return Result.ok(orderId);
    }
}
public interface IVoucherOrderService extends IService<VoucherOrder> {

    Result seckillVoucher(Long voucherId);

    Result createVoucherOrder(Long voucherId);
}

引入依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

启动类暴露代理对象:

@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
}

(三)特别说明!!!

 

        首先,这里使用了悲观锁,保证必须先获取锁,再执行调用的事务操作,最后才会释放锁,保证了安全性,不会发生事务未提交,锁就被释放的情况。

        其次, 在同一个类内直接调用另一个带有@Transactional注解的方法,如果这个调用是在同一个实例内完成的(即非代理调用),则事务不会生效。这是因为直接调用未经过代理对象,所以Spring无法插入事务管理逻辑。Spring的事务管理是基于AOP实现的,Spring AOP默认使用的是JDK动态代理,它只能代理接口中的方法或公开的方法(即public方法)。

         解决方案就是:通过AopContext.currentProxy()在同一个类内获取代理对象并调用目标方法,以此来确保事务管理等AOP增强能够在自我调用的情况下也生效。

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
// 返回订单id
return proxy.createVoucherOrder(voucherId);

        并且,使用代理对象必须暴露代理对象,在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)

        最后,引入org.aspectj:aspectjweaver依赖主要是为了支持 AspectJ 的编织功能,特别是在Spring应用中启用 AspectJ 的加载时编织。

(四)集群模式下的超领问题

1.模拟集群模式

启动这两个类,模拟两个节点的集群 

 

修改nginx.conf文件:

保存后,CMD窗口重新加载nginx 

nginx.exe -s reload

 

重复刷新该链接:http://localhost:8080/api/voucher/list/1

8081和8082的控制台都会有输出,轮询访问到两个端口,当前nginx已经有了负载均衡的效果了。 

2.断点调试 

在这两个地方打断点:

 

ApiFox设置两个一样的接口,分别访问

3.运行结果 

会注意到两次访问都会进入锁内,判断count=0,放行所有断点后,优惠券会有两个订单,同时库存也会减少2个

 

4.结果分析——锁监视器

        由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的。

        但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥。

        这就是集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Verilog代码实例

Verilog语言学习&#xff01; 文章目录 目录 文章目录 前言 一、基本逻辑门代码设计和仿真 1.1 反相器 1.2 与非门 1.3 四位与非门 二、组合逻辑代码设计和仿真 2.1 二选一逻辑 2.2 case语句实现多路选择逻辑 2.3 补码转换 2.4 7段数码管译码器 三、时序逻辑代码设计和仿真 3.1…

排序算法--基数排序

核心思想是按位排序&#xff08;低位到高位&#xff09;。适用于定长的整数或字符串&#xff0c;如例如&#xff1a;手机号、身份证号排序。按数据的每一位从低位到高位&#xff08;或相反&#xff09;依次排序&#xff0c;每次排序使用稳定的算法&#xff08;如计数排序&#…

图形化界面MySQL(MySQL)(超级详细)

目录 1.官网地址 1.1在Linux直接点击NO thanks…? 1.2任何远端登录&#xff0c;再把jj数据库给授权 1.3建立新用户 优点和好处 示例代码&#xff08;MySQL Workbench&#xff09; 示例代码&#xff08;phpMyAdmin&#xff09; 总结 图形化界面 MySQL 工具大全及其功能…

C++ 使用CURL开源库实现Http/Https的get/post请求进行字串和文件传输

CURL开源库介绍 CURL 是一个功能强大的开源库&#xff0c;用于在各种平台上进行网络数据传输。它支持众多的网络协议&#xff0c;像 HTTP、HTTPS、FTP、SMTP 等&#xff0c;能让开发者方便地在程序里实现与远程服务器的通信。 CURL 可以在 Windows、Linux、macOS 等多种操作系…

mapbox进阶,添加绘图扩展插件,绘制圆形

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️MapboxDraw 绘图控件二、🍀添加绘图扩…

网络工程师 (24)数据封装与解封装

一、数据封装 数据封装是指将协议数据单元&#xff08;PDU&#xff09;封装在一组协议头和尾中的过程。在OSI 7层参考模型中&#xff0c;数据从应用层开始&#xff0c;逐层向下封装&#xff0c;直到物理层。每一层都会为其PDU添加相应的协议头和尾&#xff0c;以包含必要的通信…

OSPF基础(3):区域划分

OSPF的区域划分 1、区域产生背景 路由器在同一个区域中泛洪LSA。为了确保每台路由器都拥有对网络拓扑的一致认知&#xff0c;LSDB需要在区域内进行同步。OSPF域如果仅有一个区域&#xff0c;随着网络规模越来越大&#xff0c;OSPF路由器的数量越来越多&#xff0c;这将导致诸…

C++----继承

一、继承的基本概念 本质&#xff1a;代码复用类关系建模&#xff08;是多态的基础&#xff09; class Person { /*...*/ }; class Student : public Person { /*...*/ }; // public继承 派生类继承基类成员&#xff08;数据方法&#xff09;&#xff0c;可以通过监视窗口检…

【DeepSeek】DeepSeek小模型蒸馏与本地部署深度解析DeepSeek小模型蒸馏与本地部署深度解析

一、引言与背景 在人工智能领域&#xff0c;大型语言模型&#xff08;LLM&#xff09;如DeepSeek以其卓越的自然语言理解和生成能力&#xff0c;推动了众多应用场景的发展。然而&#xff0c;大型模型的高昂计算和存储成本&#xff0c;以及潜在的数据隐私风险&#xff0c;限制了…

ZZNUOJ(C/C++)基础练习1081——1090(详解版)

目录 1081 : n个数求和 &#xff08;多实例测试&#xff09; C C 1082 : 敲7&#xff08;多实例测试&#xff09; C C 1083 : 数值统计(多实例测试) C C 1084 : 计算两点间的距离&#xff08;多实例测试&#xff09; C C 1085 : 求奇数的乘积&#xff08;多实例测试…

axios 发起 post请求 json 需要传入数据格式

• 1. axios 发起 post请求 json 传入数据格式 • 2. axios get请求 1. axios 发起 post请求 json 传入数据格式 使用 axios 发起 POST 请求并以 JSON 格式传递数据是前端开发中常见的操作。 下面是一个简单的示例&#xff0c;展示如何使用 axios 向服务器发送包含 JSON 数…

硬盘接入电脑提示格式化?是什么原因?怎么解决?

有时候&#xff0c;当你将硬盘接入电脑时&#xff0c;看到系统弹出“使用驱动器中的光盘之前需要将其格式化”的提示&#xff0c;肯定会感到十分困惑和焦虑。这种情况不仅让人担心数据丢失&#xff0c;也可能影响正常使用。为什么硬盘会突然要求格式化&#xff1f;是硬盘出了问…

使用Python实现PDF与SVG相互转换

目录 使用工具 使用Python将SVG转换为PDF 使用Python将SVG添加到现有PDF中 使用Python将PDF转换为SVG 使用Python将PDF的特定页面转换为SVG SVG&#xff08;可缩放矢量图形&#xff09;和PDF&#xff08;便携式文档格式&#xff09;是两种常见且广泛使用的文件格式。SVG是…

【大数据技术】搭建完全分布式高可用大数据集群(Kafka)

搭建完全分布式高可用大数据集群(Kafka) kafka_2.13-3.9.0.tgz注:请在阅读本篇文章前,将以上资源下载下来。 写在前面 本文主要介绍搭建完全分布式高可用集群 Kafka 的详细步骤。 注意: 统一约定将软件安装包存放于虚拟机的/software目录下,软件安装至/opt目录下。 安…

【C++篇】C++11新特性总结1

目录 1&#xff0c;C11的发展历史 2&#xff0c;列表初始化 2.1C98传统的{} 2.2&#xff0c;C11中的{} 2.3&#xff0c;C11中的std::initializer_list 3&#xff0c;右值引用和移动语义 3.1&#xff0c;左值和右值 3.2&#xff0c;左值引用和右值引用 3.3&#xff0c;…

Redis --- 使用HyperLogLog实现UV(访客量)

UV 和 PV 是网站或应用数据分析中的常用指标&#xff0c;用于衡量用户活跃度和页面访问量。 UV (Unique Visitor 独立访客)&#xff1a; 指的是在一定时间内访问过网站或应用的独立用户数量。通常根据用户的 IP 地址、Cookies 或用户 ID 等来唯一标识一个用户。示例&#xff1…

【机器学习案列】糖尿病风险可视化及预测

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

单片机之基本元器件的工作原理

一、二极管 二极管的工作原理 二极管是一种由P型半导体和N型半导体结合形成的PN结器件&#xff0c;具有单向导电性。 1. PN结形成 P型半导体&#xff1a;掺入三价元素&#xff0c;形成空穴作为多数载流子。N型半导体&#xff1a;掺入五价元素&#xff0c;形成自由电子作为多…

llama.cpp GGUF 模型格式

llama.cpp GGUF 模型格式 1. Specification1.1. GGUF Naming Convention (命名规则)1.1.1. Validating Above Naming Convention 1.2. File Structure 2. Standardized key-value pairs2.1. General2.1.1. Required2.1.2. General metadata2.1.3. Source metadata 2.2. LLM2.2.…

Conmi的正确答案——Rider中添加icon作为exe的图标

C#版本&#xff1a;.net 8.0 Rider版本&#xff1a;#RD-243.22562.250&#xff08;非商业使用版&#xff09; 1、添加图标到解决方案下&#xff1a; 2、打开“App.xaml”配置文件&#xff0c;添加配置&#xff1a; <Applicationx:Class"ComTransmit.App"xmlns&q…