Redission实现分布式锁之源码解析

news2024/11/27 0:23:23

Redission实现分布式锁之源码解析

  • 1、Redission实现分布式锁之源码解析
    • 1.1 分布式锁-redission功能介绍
    • 1.2 分布式锁-Redission快速入门
    • 1.3 分布式锁-redission可重入锁原理
    • 1.4 分布式锁-redission锁重试和WatchDog机制
    • 1.5 分布式锁-redission锁的MutiLock原理

1、Redission实现分布式锁之源码解析

1.1 分布式锁-redission功能介绍

接上篇Redis分布式锁原理之实现秒杀抢优惠卷业务
基于setnx实现的分布式锁存在下面的问题:

重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

**超时释放:**我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患

主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

在这里插入图片描述

那么什么是Redission呢

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能

在这里插入图片描述

1.2 分布式锁-Redission快速入门

引入依赖:

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

如何使用Redission的分布式锁

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }
    
    
    
}

在 VoucherOrderServiceImpl

注入RedissonClient

@Resource
private RedissonClient redissonClient;

@Override
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("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

1.3 分布式锁-redission可重入锁原理

在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。

在redission中,我们的也支持支持可重入锁

在分布式锁中,他采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,所以接下来我们一起分析一下当前的这个lua表达式

这个地方一共有3个参数

KEYS[1] : 锁名称

ARGV[1]: 锁失效时间

ARGV[2]: threadId; 锁的小key

exists: 判断数据是否存在 name:是lock是否存在,如果==0,就表示当前这把锁不存在

redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构

Lock{

​ threadId : 1

}

如果当前这把锁存在,则第一个条件不满足,再判断

redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

将当前这个锁的value进行+1 ,redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再对其设置过期时间,如果以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间

如果小伙伴们看了前边的源码, 你会发现他会去判断当前这个方法的返回值是否为null,如果是null,则对应则前两个if对应的条件,退出抢锁逻辑,如果返回的不是null,即走了第三个分支,在源码处会进行while(true)的自旋抢锁。

"if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);"

在这里插入图片描述

1.4 分布式锁-redission锁重试和WatchDog机制

说明:触发锁重试机制的前提必须指定tryLock方法的等待时间参数,下面进行源码跟踪。

进入内部tryLock方法块,执行流程如下:

@Override
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);   // 将等待时间的单位转为毫秒
        long current = System.currentTimeMillis();   // 记录当前时间
        long threadId = Thread.currentThread().getId();
        // 上来直接尝试获取,我们知道获取获取锁的lua脚本返回值有null和pttl;
        // null代表获取成功;
        // pttl代表锁的过期剩余时间
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId); 
        if (ttl == null) {  // 条件成立代表锁获取成功
            return true;
        }
        // 获取锁失败, 验证获取锁的等待时间(time参数)是否消耗殆尽
        time -= System.currentTimeMillis() - current;   // 剩余等待时间 = 等待时间 - 第一次获取锁失败的消耗时间
        if (time <= 0) {  // 条件成立,获取锁失败并执行结束,返回false
            acquireFailed(waitTime, unit, threadId); 
            return false;
        }
        // 还有剩余的等待时间,进行锁重试
        current = System.currentTimeMillis();  // 记录当前时间  
        /**
         * 2.订阅锁释放事件,在执行unLock方法的lua脚本后会发布锁释放消息
         * 此时通过 await 方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题:
         * 基于信息量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争.
         *
         * 当 subscribeFuture.await 返回 false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败.
         * 当 subscribeFuture.await 返回 true,进入循环尝试获取锁.
         */
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
		
		// subscribeFuture.await 返回 true,上面代码块的条件不成立,不走,进入循环重试获取锁
        try {
            time -= System.currentTimeMillis() - current;   // 剩余等待时间 = 等待时间 - 第一次获取锁失败的消耗时间
            if (time <= 0) {  // 条件成立,获取锁失败并执行结束,返回false
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        	// while(True)循环重试获取锁
            while (true) {
                long currentTime = System.currentTimeMillis();  // 记录当前时间 
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);  // 尝试获取锁,这里的逻辑和开头的代码一致
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
                // 还有剩余等待时间,再次获取当前时间
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                    //如果剩余时间(ttl)小于wait time ,就在 ttl 时间内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可),等待并获取其它线程释放的信号
                    subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    //则就在wait time 时间范围内等待可以接收的信号量
                    subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }
				// 剩余等待时间 = 等待时间 - 第一次获取锁失败的消耗时间
                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {  // 等待时间耗尽,结束
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
                // 走到这里,无代码了,继续循环,继续重试获取锁
            }
        } finally {
            // 无论是否获得锁,都要取消订阅解锁消息
            unsubscribe(subscribeFuture, threadId);
        }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
    }

现在理解了锁的重试机制,明白未抢到锁的线程,会根据等待时间是否充分,去决定是否重试获取锁。
假设线程1获取锁成功了,但是在执行业务遇到阻塞情况,此时pttl过期了,导致锁超时释放。另一个哥们(线程2)接收到前面线程1的pttl过期并释放锁的信号,随后线程2获取锁后就出现线程安全问题了。这个问题该怎么解决呢?没错,接下来就要讲到我们Redission的看门狗机制了。

首先进入tryLock方法,跟进一下调用链,来到tryAcquireOnceAsync方法:

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1) {
            return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }
        // //当leaseTime = -1 时,启动 watch dog机制,getLockWatchdogTimeout()默认指定过期时间为30s
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                    commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        // 执行完lua脚本后的回调
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // 成功获取锁,更新有效期,内部源码通过定时任务每隔10s,定时重置有效期
            if (ttlRemaining) {
            	// 跟进此方法
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

private void scheduleExpirationRenewal(long threadId) {
		// 一个锁就对应自己的一个ExpirationEntry类实例对象
        ExpirationEntry entry = new ExpirationEntry()
        // EXPIRATION_RENEWAL_MAP是一个线程安全的ConcurrentMap集合;
        // putIfAbsent方法添加键值对,如果添加的键在集合中不存在,即添加成功,返回null;
        // putIfAbsent方法添加键值对,如果添加的键在集合中存在,即添加失败,不替换value,返回原有的value值;
        // 下面利用集合存储<EntryName是指锁的名称:entry对象>键值对
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {   // 满足条件,代表线程进行锁的重入,不进行延期操作
        	// 将线程ID加入
            oldEntry.addThreadId(threadId);
        } else {  // 第一次获取锁,进行延期操作
        	// 将线程ID加入
            entry.addThreadId(threadId);
            // 延期方法,下面跟进这个方法
            renewExpiration();
        }
    }

private void renewExpiration() {
	// 根据锁的名称获取entry对象
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    //  如果集合不存在,那不再锁续期
    if (ee == null) {
        return;
    }
    
    //这个是一个延迟任务,每隔10秒执行一次
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        
        //延迟任务内容
        @Override
        public void run(Timeout timeout) throws Exception {
        	// 根据锁的名称获取entry对象
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            //  如果集合不存在,那不再锁续期
            if (ent == null) {
                return;
            }
			// 获取在scheduleExpirationRenewal中存入的线程id
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 线程不为空,调用renewExpirationAsync方法刷新过期时间,重置为30s
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {  // res:返回值   e:异常信息
                if (e != null) {  
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {  // 续期成功
                    //renewExpirationAsync方法执行成功之后,进行递归调用,调用自己本身函数
                    //那么就可以实现这样的效果
                    //首先第一次进行这个函数,设置了一个延迟任务,在10s后执行
                    //10s后,执行延迟任务的内容,刷新有效期成功,那么就会再新建一个延迟任务,刷新过期时间
                    //这样这个过期时间就会一直续费
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);  // internalLockLeaseTime/3=10秒执行一次
    
    ee.setTimeout(task);
}

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
		// 执行lua脚本,给过期时间进行续期30s
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return 0;",
                Collections.singletonList(getName()),
                internalLockLeaseTime, getLockName(threadId));
    }
    

现在到了这步,看门狗就会一直进行续期,什么时候结束呢?当然是在释放锁的过程中,下面是释放锁的方法:

public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise<Void>();
        // unlockInnerAsync方法执行释放锁的lua脚本
        RFuture<Boolean> future = unlockInnerAsync(threadId);
		
		// 回调方法
        future.onComplete((opStatus, e) -> {
        	// 无论lua脚本是否执行成功,都会执行cancelExpirationRenewal方法来删除EXPIRATION_RENEWAL_MAP中的缓存
            cancelExpirationRenewal(threadId);

            if (e != null) {
                result.tryFailure(e);
                return;
            }

            if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                        + id + " thread-id: " + threadId);
                result.tryFailure(cause);
                return;
            }

            result.trySuccess(null);
        });

        return result;
    }

void cancelExpirationRenewal(Long threadId) {
        ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (task == null) {
            return;
        }
        
        if (threadId != null) {   // 删除entry对象中的线程id
            task.removeThreadId(threadId);
        }

        if (threadId == null || task.hasNoThreads()) {
            Timeout timeout = task.getTimeout();
            if (timeout != null) {
                timeout.cancel();
            }
            // 删除集合中此锁对应的entry对象缓存,这样看门狗那边就获取不到此对象,停止续期了
            EXPIRATION_RENEWAL_MAP.remove(getEntryName());
        }
    }

如果释放锁操作本身异常了,watch dog 还会不停的续期吗?
不会。因为无论在释放锁的时候,是否出现异常,都会执行释放锁的回调函数,把看门狗停了

有没有设想过一种场景?服务器宕机了?其实这也没关系,首先获取锁和释放锁的逻辑都是在一台服务器上,那看门狗的续约也就没有了,redis中只有一个看门狗上次重置了30秒的key,时间到了key也就自然删除了,那么其他服务器,只需要等待redis自动删除这个key就好了,也就不存在死锁了

总结
(1)watchDog 在当前节点存活时,每10s给分布式锁的key续期 30s

(2)watchDog 最终还是通过 Lua脚本的expire命令来进行重置有效期,更新有效期

(3)watchDog 机制启动,且代码中没有释放锁操作时,watchDog 会不断的给锁续期

(4)要使 watchDog 机制生效 ,就不要设置锁过期时间 leaseTime

(5)锁续期是通过一个定时任务,在 renewExpiration() 中自己调自己实现的!

1.5 分布式锁-redission锁的MutiLock原理

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

在这里插入图片描述

为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

在这里插入图片描述

那么MutiLock 加锁原理是什么呢?笔者画了一幅图来说明

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

在这里插入图片描述

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

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

相关文章

5G+工业互联网观察——应用篇

5G与工业互联网的结合是5G应用的重要领域&#xff0c;前一篇《5G工业互联网观察——政策篇》我们对5G工业互联网的相关政策进行了整理和分析&#xff0c;本篇继续从应用的角度整理目前的典型场景和重点行业&#xff0c;并进行简单的分析。 文 | 无界 全文4500字&#xff0c;预计…

原创 | 一文读懂蒙特卡洛算法

作者&#xff1a;陈之炎本文约2000字&#xff0c;建议阅读10分钟 本文介绍了蒙特卡洛算法。 蒙特卡洛算法&#xff08;Monte Carlo algorithm&#xff09;是一种基于随机采样的计算方法&#xff0c;其基本思想是通过生成随机样本&#xff0c;利用统计学原理来估计数学问题的解。…

MySQL实战之主从数据同步机制

主从同步的重要性&#xff1a; 解决数据可靠性的问题需要用到主从同步&#xff1b;解决 MySQL 服务高可用要用到主从同步&#xff1b;应对高并发的时候&#xff0c;还是要用到主从同步。 一、MySQL 主从同步流程 当客户端提交一个事务到 MySQL 的集群&#xff0c;直到客户端收…

《JavaEE初阶》Servlet

《JavaEE初阶》Servlet 文章目录 《JavaEE初阶》Servlet编写一个Servlet的helloworld1. 认识Maven并创建maven项目:2. 引入依赖:3.创建目录:4. 编写代码:5. 打包6. 部署7.验证 优化打包部署操作.常见的错误: ServletAPI:利用ajax构造请求.使用第三方工具postman构造请求HttpSer…

KDXL-A工频输电线路参数测试仪

一、简介 由我公司开发、研制的专门用于输电线路工频参数测量的高精度仪器&#xff0c;对于输电线路的一系列工频参数可进行精密的测量。 KDXL-A输电线路参数测试仪具有体积小、重量轻、测量准确度高、稳定性好、操作简便易学等优点,可取代以往利用多表法测量线路参数的方法&am…

springboot-热部署

什么是热部署 事先我创建一个springboot项目&#xff0c;引入web依赖。 当我冷启动的时候&#xff0c;日志如下&#xff1a; D:\tools\jdk-17.0.3\bin\java.exe -XX:TieredStopAtLevel1 -noverify -Dspring.output.ansi.enabledalways -Dcom.sun.management.jmxremote -Dspr…

【神经网络】tensorflow -- 期中测试试题

题目一&#xff1a;&#xff08;20分&#xff09; 请使用Matplotlib中的折线图工具&#xff0c;绘制正弦和余弦函数图像&#xff0c;其中x的取值范围是&#xff0c;效果如图1所示。 要求&#xff1a; (1)正弦图像是蓝色曲线&#xff0c;余弦图像是红色曲线&#xff0c;线条宽度…

洛谷B2097 最长平台

最长平台 题目描述 对于一个数组&#xff0c;其连续的相同段叫做一个平台&#xff0c;例如&#xff0c;在 1 1 1&#xff0c; 2 2 2&#xff0c; 2 2 2&#xff0c; 3 3 3&#xff0c; 3 3 3&#xff0c; 3 3 3&#xff0c; 4 4 4&#xff0c; 5 5 5&#xff0c; 5 5 5&…

加密解密软件VMProtect入门使用教程(四):准备项目

VMProtect是新一代软件保护实用程序。VMProtect支持德尔菲、Borland C Builder、Visual C/C、Visual Basic&#xff08;本机&#xff09;、Virtual Pascal和XCode编译器。 同时&#xff0c;VMProtect有一个内置的反汇编程序&#xff0c;可以与Windows和Mac OS X可执行文件一起…

【2023 年第三届长三角高校数学建模竞赛】B 题 长三角新能源汽车发展与双碳关系研究 新能源汽车销售相关数据160M+下载

【2023 年第三届长三角高校数学建模竞赛】B 题 长三角新能源汽车发展与双碳关系研究 新能源汽车销售相关数据160M下载 1 题目 《节能与新能源汽车技术路线图 2.0》提出至 2035 年&#xff0c;新能源汽车市场占比超过 50%&#xff0c;燃料电池汽车保有量达到 100 万辆&#xff…

想做外贸却没有头绪?来看看这篇文章

海关总署公布最新数据&#xff1a;今年前4个月&#xff0c;我国外贸进出口总值13.32万亿元&#xff0c;同比增长5.8%&#xff0c;其中出口7.67万亿元&#xff0c;同比增长10.6%&#xff1b;进口5.65万亿元&#xff0c;同比增长0.02%。月度调查显示&#xff0c;出口订单增加的企…

图解LeetCode——238. 除自身以外数组的乘积

一、题目 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请不要使用除法&#xff0c;且在 O(n…

SoringBoot——pom文件:starter

先来看一看&#xff1a; 这次我们来介绍SpringBoot的pom文件的另一个好玩的地方&#xff1a;starter。 starter的中文含义是启动器&#xff0c;所以有时候我们在Maven仓库找依赖的时候&#xff0c;如果开启了自动翻译就会经常会看见一个奇怪的词叫做某某弹簧启动器&#xff0…

2023年河北沃克高位承重货架最新中标项目|中国沈阳某大型集团高位重型横梁式货架项目建设初期

【项目名称】高位重型横梁式货架项目 【承建单位】河北沃克金属制品有限公司 【合作客户】中国沈阳某大型集团 【建设时间】2023年5月上旬 【建设地域】中国沈阳地区 【项目客户需求】 本次沈阳高位重型横梁式货架项目合作的沈阳某大型集团中国变压器行业规模最大的制造企…

AIGC的发展与机遇

陈老老老板&#x1f9b8; &#x1f468;‍&#x1f4bb;本文专栏&#xff1a;赠书活动专栏&#xff08;为大家争取的福利&#xff0c;免费送书&#xff09;试一下文末赠书&#xff0c;讲一些科普类的知识 &#x1f468;‍&#x1f4bb;本文简述&#xff1a;本篇内容的引用都已征…

Protobuf: 高效数据传输的秘密武器

当涉及到网络通信和数据存储时&#xff0c;数据序列化一直都是一个重要的话题&#xff1b;特别是现在很多公司都在推行微服务&#xff0c;数据序列化更是重中之重&#xff0c;通常会选择使用 JSON 作为数据交换格式&#xff0c;且 JSON 已经成为业界的主流。但是 Google 这么大…

《理解了实现再谈网络性能》读书笔记

文章目录 内核是如何接收网络包的1.1 Linux⽹络收包总览1.2 linux 启动创建ksoftirqd进程网络子系统初始化协议栈注册网卡驱动初始化启动网卡 1.3 迎接数据的到来硬中断处理ksoftirqd 内核线程处理软中断网络协议栈处理IP协议层处理 完整流程 内核是如何接收网络包的 1.1 Linu…

使用阿里云服务器建站WordPress博客网站上线全流程

使用阿里云服务器快速搭建网站教程&#xff0c;先为云服务器安装宝塔面板&#xff0c;然后在宝塔面板上新建站点&#xff0c;阿里云服务器网以搭建WordPress网站博客为例&#xff0c;来详细说下从阿里云服务器CPU内存配置选择、Web环境、域名解析到网站上线全流程&#xff1a; …

常见概率分布及图像

概率分布 文章目录 概率分布[toc]1 离散概率分布1.1 伯努利分布1.2 二项分布1.3 泊松分布 2 连续概率分布2.1 均匀分布2.2 指数分布2.3 正态分布2.4 卡方分布2.5 Student分布3.5 F分布 1 离散概率分布 1.1 伯努利分布 随机变量 X X X仅取两个值&#xff0c; X 0 , 1 X0,1 X0…

【2023 年第三届长三角高校数学建模竞赛】C 题 考研难度知多少 考研情况相关数据下载

【2023 年第三届长三角高校数学建模竞赛】C 题 考研难度知多少 1 题目 C 题 考研难度知多少 据相关媒体报道&#xff0c;2023 年考研可以称得上是“最难”的一年&#xff0c;全国研究生报考人数突破新高达到 474 万人、部分考研学生感染新冠带病赴考、保研名额增多 挤压考研…