三、优惠卷秒杀

news2024/11/10 23:02:59

文章目录

      • 优惠卷秒杀
        • 1.redis实现分布式ID
        • 2.优惠券秒杀下单
        • 3.超卖问题
        • 4.lua脚本
        • 5.分布式锁
        • 6.redis stream消息队列实现异步秒杀
        • 7.redis消息队列
          • list实现消息队列
          • PubSub实现消息队列
          • stream实现消息队列
          • stream的消息队列-消费者组

学习黑马点评项目整理总结:https://www.bilibili.com/video/BV1cr4y1671t/?vd_source=5f3396d3af2c3945929d54c786f289e5

官方命令文档:https://redis.io/commands/

优惠卷秒杀

1.redis实现分布式ID

在分布式系统下,普遍要进行分库分表,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

  • 唯一性
  • 高可用
  • 高性能
  • 递增性
  • 安全性

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
在这里插入图片描述

ID的组成部分:符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

实现

@Component
public class RedisIdWorker {

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    @Autowired
    private StringRedisTemplate redisTemplate;

    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"));
        Long count = redisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        // timestamp:33919388
        // count:53293
        // 结果:145682662160388141(十进制)
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        Calendar now = Calendar.getInstance();
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long timeInMillis = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(timeInMillis);

        RedisIdWorker redisIdWorker = new RedisIdWorker();
        long order = redisIdWorker.nextId("order");
    }
}

timestamp << COUNT_BITS | count;
// timestamp:33919388
// count:53293
// 结果:145682662160388141(十进制)->001000000101100100011001110000000000000000001101000000101101(二进制)
时间戳向左移动32位,低位以0补位,再与count做或运算,就组成了一个id

测试

@Autowired
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(300);

    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();
    };
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 300; i++) {
        es.submit(task);
    }
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}

2.优惠券秒杀下单

总共分3步:
1.校验秒杀是否开始或结束,以及库存是否充足
2.扣减库存
3.生成优惠券订单

伪代码:

public 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("库存不足!");
    }
    //5,扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1")
            .eq("voucher_id", voucherId).update();
            
    //6.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    // 6.1.订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 6.2.用户id
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    // 6.3.代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);

    return Result.ok(orderId);
}

3.超卖问题

分析:假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。
在这里插入图片描述
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:而对于加锁,我们通常有两种解决方案:

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

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。

乐观锁主要有两种:

  • 版本号
  • CAS

1.版本号法
线程1先查出库存和版本号,这时线程2也来了,查询出相同的库存和版本号,线程1执行扣减操作成功,并且库存的版本号加一,表示被修改过了,线程二再进行扣减用的version=1,数据库中已经不存在版本号为1的数据,扣减失败
在这里插入图片描述

2.CAS法
线程1先查出库存,这时线程2也来了,查询出相同的库存,线程1执行扣减操作成功,线程二再进行扣减用的stock=1,数据库中已经不存在库存为1的数据,扣减失败
在这里插入图片描述

总结:
悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般
    乐观锁:不加锁,在更新时判断是否有其它线程在修改
  • 优点:性能好
  • 缺点:存在成功率低的问题

乐观锁比较适合更新数据,悲观锁比较适合增删数据

4.lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了,作为Java程序员这一块并不作一个简单要求,并不需要大家过于精通,只需要知道他有什么作用即可。

这里重点介绍Redis提供的调用函数,语法如下:

redis.call('命令名称', 'key', '其它参数', ...)

例如,我们要执行set name jack,则脚本是这样:

# 执行 set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name Rose,再执行get name,则脚本如下:

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
在这里插入图片描述
例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:
在这里插入图片描述
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
在这里插入图片描述

5.分布式锁

项目在集群环境下使用syn加锁依然会有线程安全问题,每个项目就是一个tomcat,也就是部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。
在这里插入图片描述

分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
在这里插入图片描述
常见分布式锁实现:
在这里插入图片描述
手动实现redis分布式锁,功能如下:
1.加锁
2.释放锁
3.过期时间兜底(防死锁)
4.防误删(释放锁先判断是否是自己的锁)
5.lua脚本保证原子性

代码如下

public class SimpleRedisLock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.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<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

释放锁脚本unlock.lua

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

自己实现的基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁
    • 特性:
      • 利用set nx满足互斥性
      • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
      • 利用Redis集群保证高可用和高并发特性

但还有锁续期功能没有实现,自己实现起来很复杂,推荐使用redission

6.redis stream消息队列实现异步秒杀

现在的秒杀流程是同步的:
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单

在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢,所以我们需要异步程序执行。

修改需求:

  • 1.新增秒杀优惠券的同时,将优惠券信息保存到Redis中
  • 2.基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  • 3.如果抢购成功,将优惠券id和用户id封装后存入stream消息队列
  • 4.不断从消息队列中获取信息,实现异步下单功能

现将前4步使用lua脚本实现,5,6步扔进消息队列中异步执行
1.新增优惠券

@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中
    //private static final String SECKILL_STOCK_KEY ="seckill:stock:"
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

2和3.秒杀脚本seckill.lua

-- 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.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

4.从消息队列读取消息,实现下单

使用线程池在项目启动时阻塞读取消息队列

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

private class VoucherOrderHandler implements Runnable {

    @Override
    public 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.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "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 0
                List<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.创建订单
                createVoucherOrder(voucherOrder);
                // 4.确认消息 XACK
                stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
            } catch (Exception e) {
                log.error("处理订单异常", e);
            }
        }
    }
}

下单方法

private void createVoucherOrder(VoucherOrder voucherOrder) {
    Long userId = voucherOrder.getUserId();
    Long voucherId = voucherOrder.getVoucherId();
    // 创建锁对象
    RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    // 尝试获取锁
    boolean isLock = redisLock.tryLock();
    // 判断
    if (!isLock) {
        // 获取锁失败,直接返回失败或者重试
        log.error("不允许重复下单!");
        return;
    }

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            log.error("不允许重复下单!");
            return;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足!");
            return;
        }

        // 7.创建订单
        save(voucherOrder);
    } finally {
        // 释放锁
        redisLock.unlock();
    }
}

7.redis消息队列

什么是消息队列:字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息
    在这里插入图片描述
    使用队列的好处在于 **解耦:**所谓解耦,举一个生活中的例子就是:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间,所以这种思想在我们日常开发中,是非常有必要的。

这种场景在我们秒杀中就变成了:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。

这里我们可以使用一些现成的mq,比如kafka,rabbitmq等等,但是呢,如果没有安装mq,我们也可以直接使用redis提供的mq方案,降低我们的部署和学习成本。

list实现消息队列

消息队列(Message Queue),字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果。

队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。
不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
在这里插入图片描述
基于List的消息队列有哪些优缺点?
优点:

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

缺点:

  • 无法避免消息丢失
  • 只支持单消费者
PubSub实现消息队列

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

SUBSCRIBE channel [channel] :订阅一个或多个频道
PUBLISH channel msg :向一个频道发送消息
PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道
在这里插入图片描述
基于PubSub的消息队列有哪些优缺点?
优点:

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

缺点:

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

Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
STREAM类型消息队列的XREAD命令特点:

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

消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
在这里插入图片描述
创建消费者组:

XGROUP CREATE key groupname <id | $> [MKSTREAM] [ENTRIESREAD entries_read]

key:队列名称
groupName:消费者组名称
ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
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 ...]
  • group:消费组名称
  • consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
  • count:本次查询的最大数量
  • BLOCK milliseconds:当没有消息时最长等待时间
  • NOACK:无需手动ACK,获取到消息后自动确认
  • STREAMS key:指定队列名称
  • ID:获取消息的起始ID:

“>”:从下一个未消费的消息开始
其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始

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

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

对比
在这里插入图片描述

实现看5.redis stream消息队列实现异步秒杀,具体需求如下:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

腾达Tenda路由器中继wifi步骤

前提&#xff1a; 你有一个信号比较弱&#xff0c;但能上网的wifi&#xff08;暂时叫它1号wifi&#xff09;&#xff0c;并知道其密码你有一个有中继功能的路由器&#xff0c;比如Tenda某型号路由器&#xff0c;插上电&#xff0c;这个路由器的wifi暂时叫它2号wifi 长按下拉菜…

数据库面试题总结

文章目录一、索引相关&#xff08;1&#xff09;什么是索引?&#xff08;2&#xff09;索引是个什么样的数据结构呢?&#xff08;3&#xff09;为什么使用索引&#xff1f;&#xff08;4&#xff09;主键和索引的区别?&#xff08;5&#xff09;说一说索引的底层实现&#x…

educoder数据结构 排序 第2关:实现快速排序

本文已收录于专栏 &#x1f332;《educoder数据结构与算法》&#x1f332; 任务描述 本关要求通过补全快速排序私有函数QSort__来供函数QuickSort调用&#xff0c;以此来实现快速排序的功能。 相关知识 快速排序的基本过程是&#xff1a;从待排序记录中任选一个记录&#…

MS-Model【2】:nnFormer

文章目录前言1. Abstract & Introduction1.1. Abstract1.2. Introduction1.3. Related work2. Method2.1. Overview2.2. Encoder2.2.1. Components2.2.2. The embedding layer2.2.3. Local Volume-based Multi-head Self-attention (LV-MSA)2.2.4. The down-sampling layer…

【通信原理(含matlab程序)】实验五:二进制数字调制与解调

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; 本人持续分享更多关于电子通信专业内容以及嵌入式和单片机的知识&#xff0c;如果大家喜欢&#xff0c;别忘点个赞加个关注哦&#xff0c;让我们一起共同进步~ &#x…

Arduino的45种传感器测试(初级)

前言 说是Arduino的传感器&#xff0c;实际只要明白接口通信方式&#xff0c;其他开发板也可以使用。这一篇的测试是对一些开关和led等的测试&#xff0c;只使用了3.3v / 5v电源和万用表就可完成。 震动开关 实物图和原理图如下 原理&#xff1a;中心有一个金属线的空心黑…

Java多线程-Thread的Object类介绍【wait】【notify】【sleep】

Thread和Object类详解 方法概览 Thread wait、notify、notifyAll方法详解 作用 阻塞阶段 使用了wait方法之后&#xff0c;线程就会进入阻塞阶段&#xff0c;只有发生以下四种情况中的其中一个&#xff0c;线程才会被唤醒 另一个线程调用了这个线程的notify方法&#xff0…

Python数据可视化之直方图和密度图

Python数据可视化之直方图和密度图 提示&#xff1a;前言 Python数据可视化之直方图和密度图 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录Python数据可视化之直方图和密度图前言一、导入包二、选择数据集三、直…

k8s之平滑升级

写在前面 通过POD 应用就有了存在的形式&#xff0c;通过deployment 保证了POD在一定的数量&#xff0c;通过service 可以实现一定数量的POD以负载均衡的方式对外提供服务。但&#xff0c;如果是程序开发了新功能&#xff0c;需要上线&#xff0c;该怎么办呢&#xff1f;对此k…

jvm相关,jvm内存模型,java程序运行流程及jvm各个分区的作用、对象的组成(针对hotspot虚拟机)--学习笔记

java程序运行时的运行模型 在jdk1.8之前的元空间&#xff0c;称为永久代并将元空间挪到了堆直接使用本地内存&#xff0c;不再占用堆空间 jvm内存结构划分 堆&#xff08;方法区&#xff09;和元空间是线程共有的&#xff0c;其他部分是线程私有的 每创建一个线程都会创建一个…

MYSQL中常见的知识问题(二)

1、B树和B树的区别&#xff0c;MYSQL为啥使用B树。 1.1、B树 目的&#xff1a;为了存储设备或者磁盘设计的一种平衡查找树。 定义&#xff08;M阶B树&#xff09;&#xff1a;a、树中的每个节点最多有m个孩子。 b、除了根节点和叶子节点外&#xff0c;其他节点最少含有m/2(取上…

08-网络管理-iptables基础(四表五链、禁止ping、防火墙规则添加/删除、自建链使用、SNAT\DNAT模式、FTP服务器防火墙规则)待发布

文章目录1. 概述1.1 四表1.2 五链1.3 四表五链的关系1.4 使用流程2. 语法和操作1.1 语法1.2 常用操作命令1.3 基本匹配条件1.4 基本动作1.5 常用命令示例- 设置默认值- 禁止80端口访问- 查看防火墙规则- 保存规则- 允许ssh- 禁止ping- 删除规则- 清除规则&#xff08;不包括默认…

HR软件如何识别保留优秀员工

在企业信息化的时代&#xff0c;越来越多的年轻员工开始追求他们的激情&#xff0c;辞掉那些乏味的工作&#xff0c;而选择加入重视员工生活质量的企业。他们不再追随那些以牺牲员工福利为代价追求利润的公司。 员工认可度有助于加强组织中的团队合作关系&#xff0c;反过来&a…

木马程序(病毒)

木马的由来 "特洛伊木马"&#xff08;trojan horse&#xff09;简称"木马"&#xff0c;据说这个名称来源于希腊神话《木马屠城记》。古希腊有大军围攻特洛伊城&#xff0c;久久无法攻下。于是有人献计制造一只高二丈的大木马&#xff0c;假装作战马神&…

实用技巧盘点:Python和Excel交互的常用操作

大家好&#xff0c;在以前&#xff0c;商业分析对应的英文单词是Business Analysis&#xff0c;大家用的分析工具是Excel&#xff0c;后来数据量大了&#xff0c;Excel应付不过来了&#xff08;Excel最大支持行数为1048576行&#xff09;&#xff0c;人们开始转向python和R这样…

【通信原理(含matlab程序)】实验六:模拟信号的数字化

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; 本人持续分享更多关于电子通信专业内容以及嵌入式和单片机的知识&#xff0c;如果大家喜欢&#xff0c;别忘点个赞加个关注哦&#xff0c;让我们一起共同进步~ &#x…

一文理解JVM虚拟机

一. JVM内存区域的划分 1.1 java虚拟机运行时数据区 java虚拟机运行时数据区分布图&#xff1a; JVM栈&#xff08;Java Virtual Machine Stacks&#xff09;&#xff1a; Java中一个线程就会相应有一个线程栈与之对应&#xff0c;因为不同的线程执行逻辑有所不同&#xff…

【JavaGuide面试总结】Java IO篇

【JavaGuide面试总结】Java IO篇1.有哪些常见的 IO 模型?2.Java 中 3 种常见 IO 模型BIO (Blocking I/O)NIO (Non-blocking/New I/O)AIO (Asynchronous I/O)1.有哪些常见的 IO 模型? UNIX 系统下&#xff0c; IO 模型一共有 5 种&#xff1a; 同步阻塞 I/O、同步非阻塞 I/O、…

浏览器兼容性 问题产生原因 厂商前缀 滚动条 css hack 渐近增强 和 优雅降级 caniuse

目录浏览器兼容性问题产生原因厂商前缀滚动条css hack渐近增强 和 优雅降级caniuse浏览器兼容性 问题产生原因 市场竞争标准版本的变化 厂商前缀 比如&#xff1a;box-sizing&#xff0c; 谷歌旧版本浏览器中使用-webkit-box-sizing:border-box 市场竞争&#xff0c;标准没有…

Java多线程案例之线程池

前言&#xff1a;在讲解线程池的概念之前&#xff0c;我们先来谈谈线程和进程&#xff0c;我们知道线程诞生的目的其实是因为进程太过重量了&#xff0c;导致系统在 销毁/创建 进程时比较低效&#xff08;具体指 内存资源的申请和释放&#xff09;。 而线程&#xff0c;其实做…