redis四:redis实现分布式锁

news2024/9/22 5:23:59

文章目录

  • redis实现分布式锁
    • 环境搭建
    • redis手写分布式锁
    • redisson 分布式锁分析
      • springboot 整合 redisson
      • redisson原理分析
      • redisson源码分析
        • 加锁逻辑
        • 锁续命逻辑
        • redisson获取不到锁自旋逻辑
        • 解锁逻辑

redis实现分布式锁

环境搭建

搭建nginx 模拟分布式情况

upstream redissonlock{
     server 192.168.101.6:8888 weight=1;
     server 192.168.101.6:8090 weight=1;
    }

    server {
        listen 8081; #监听端口
        server_name localhost;
  location / {
    proxy_pass http://redissonlock;
    proxy_redirect default;
   }
 }

项目依赖

 <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

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

		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.9.0</version>
		</dependency>

还要用到jmeter 模拟高并发场景
在这里插入图片描述
在这里插入图片描述
200个线程 五秒内完成请求发送

通过idea 模拟分布式情景
在这里插入图片描述

redis手写分布式锁

市面上有很多分布式锁的方案,但是通过手写分布式锁,可以更好了解分布式锁的原理,以及分布式锁逻辑流程
案例代码:

@RestController
@RequestMapping(value = "/redis/demo/",method = RequestMethod.GET)
public class DemoController {
	@Autowired
	private StringRedisTemplate redisTemplate;

	@RequestMapping("/demoLock1")
	public String demoLock1(){
		synchronized (this) {
			int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
			if (stock > 0) {
				int realStock = stock - 1;
				redisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
				System.out.println("扣减成功,剩余库存:" + realStock);
			} else {
				System.out.println("扣减失败,库存不足");
			}
		}
		return "end";
	}
}

上述代码在单机情况下通过synchronized能保证并发安全性,但是在分布式场景 无法保证并发安全性。那么接下来就是对上述代码加上分布式锁
实现最简单的分布式锁
在这里插入图片描述
redis中设置商品数量为300
上述代码修改为最简单的分布式锁

public String demoLock1() {
		String lockkey = "lock:key_1001";
		//加锁
		final Boolean result = redisTemplate.opsForValue().setIfAbsent(lockkey, "test");
		if (!result) {
			return "未获取锁,不能扣减库存";
		}
		int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
		if (stock > 0) {
			int realStock = stock - 1;
			redisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
			System.out.println("扣减成功,剩余库存:" + realStock);
		} else {
			System.out.println("扣减失败,库存不足");
		}
		//删除锁
		redisTemplate.delete(lockkey);
		return "end";
	}

上诉分布式锁能够解决分布式并发的问题,但是也存在其他的问题。下面来一一解决可能出现的问题

问题一:抛异常或者宕机导致无法删除锁,导致死锁
增加过期时间,并且在final 中删除锁

public String demoLock1() {
		String lockkey = "lock:key_1001";
		//加锁
		final Boolean result = redisTemplate.opsForValue().setIfAbsent(lockkey, "test", Duration.ofSeconds(10));
		if (!result) {
			System.out.println("未获取锁,不能扣减库存");
			return "未获取锁,不能扣减库存";
		}
		try {
			int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
			if (stock > 0) {
				int realStock = stock - 1;
				redisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
				System.out.println("扣减成功,剩余库存:" + realStock);
			} else {
				System.out.println("扣减失败,库存不足");
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} finally {
			//删除锁
			redisTemplate.delete(lockkey);
		}
		return "end";
	}

上述代码在并发不过情况下获取可以,但是在高并发下就有严重bug。比如说线程1业务流程执行时间超过了锁的过期时间,导致锁失效,此时其他线程继续设置锁,然后线程1执行删除锁操作的时候,把其他线程的锁给删除了,那么就会导致一些列的锁失效的问题
问题二:业务执行时间超过锁过期时间,以及删除锁时删除不是自己设置的锁,导致锁失效
核心问题就是删除不是自己设置的锁,主要就是来解决这个问题。至于业务时间过长导致锁过期,可以增加过期时间,但是这样治标不治本,有一种方式叫做锁续命,就是说业务未执行完时,不断的给锁增加过期时间。这种方案要实现并不容易,也没有必要重复造轮子

public String demoLock1() {
		String lockkey = "lock:key_1001";
		String uuid = UUID.randomUUID().toString();
		//加锁
		final Boolean result = redisTemplate.opsForValue().setIfAbsent(lockkey, uuid, Duration.ofSeconds(10));
		if (!result) {
			System.out.println("未获取锁,不能扣减库存");
			return "未获取锁,不能扣减库存";
		}
		try {
			int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
			if (stock > 0) {
				int realStock = stock - 1;
				redisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
				System.out.println("扣减成功,剩余库存:" + realStock);
			} else {
				System.out.println("扣减失败,库存不足");
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} finally {
			//是自己设置的锁才删除
			if (uuid.equals(redisTemplate.opsForValue().get(lockkey))){
				redisTemplate.delete(lockkey);
			}
		}
		return "end";
	}

这里增加了一个uuid用来判断是否是当前线程设置的锁,如果是才能删除。当时上述代码任然有问题

if (uuid.equals(redisTemplate.opsForValue().get(lockkey))){
				redisTemplate.delete(lockkey);
			}

这部分代码不是原子性,如果判断成功后,系统卡顿,正好此时锁过期了,其他线程设置了锁,然后卡顿恢复在执行删除代码,任然会删除其他线程设置的锁
问题三:判断锁的逻辑和删除锁不是原子性,任然有可能删除其他线程的锁
采用lua脚本实现原子性,关于lua脚本后面的redisson 源码分析的过程中会分析

redisson 分布式锁分析

springboot 整合 redisson

增加一个bean

@Bean
    public Redisson redisson() {
        // 此为单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.101.106:6379").setDatabase(0);
        //集群模式
//        final ClusterServersConfig clusterServersConfig = config.useClusterServers();
//        clusterServersConfig.addNodeAddress("redis://192.168.101.106:6379","redis://192.168.101.107:6379");
//        clusterServersConfig.setPassword("aaaa");
        return (Redisson) Redisson.create(config);
    }

获取redisson

@Autowired
private Redisson redisson;

实例代码

public String demoRedisson() {
		String lockkey = "lock:key_1001";
		//获取分布式锁
		final RLock lock = redisson.getLock(lockkey);
		//加锁
		lock.lock();
		try {
			int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
			if (stock > 0) {
				int realStock = stock - 1;
				redisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
				System.out.println("扣减成功,剩余库存:" + realStock);
			} else {
				System.out.println("扣减失败,库存不足");
			}
		} catch (NumberFormatException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
		return "end";
	}

redisson原理分析

redisson流程图
在这里插入图片描述
首先线程1向redis获取锁 类似执行 SETNX 命令,获取锁
线层2向redis获取锁,获取失败 进行间歇性while自选
线程1获取锁成功,后台线程每隔10秒检查是否还持有锁,如果持有延长锁的过期时间
直到线程1释放锁,线程2开始获取锁

redisson源码分析

加锁逻辑

		//获取分布式锁
		final RLock lock = redisson.getLock(lockkey);
		//加锁
		lock.lock();

在这里插入图片描述
在这里插入图片描述
注意这两个参数 一个是-1 一个是null

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        //获取当前线程
        long threadId = Thread.currentThread().getId();
        //核心加锁逻辑
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        if (ttl != null) {
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);
            try {
                while(true) {
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } else {
                        this.getEntry(threadId).getLatch().acquire();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }

Long ttl = this.tryAcquire(leaseTime, unit, threadId);
在这里插入图片描述

 private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
        	//具体加锁逻辑
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            //加锁成功调用这个方法
            ttlRemainingFuture.addListener(new FutureListener<Long>() {
                public void operationComplete(Future<Long> future) throws Exception {
                    if (future.isSuccess()) {
                    	//拿到RFuture的返回值
                        Long ttlRemaining = (Long)future.getNow();
                       //等于null表示加锁成功
                        if (ttlRemaining == null) {
                        	//锁续命逻辑
                            RedissonLock.this.scheduleExpirationRenewal(threadId);
                        }

                    }
                }
            });
            return ttlRemainingFuture;
        }
    }

先来看具体的加锁逻辑

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "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]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

主要就是执行lua脚本,这里解释一下,其中KEYS[1] 相当于一个占位符 表示Collections.singletonList(this.getName())的第一个值,如果是KEYS[2] 那么就是Collections.singletonList(this.getName())的第二个值,以此类推。
ARGV[1] 也是占位符 表示 new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)} 的第一个值
ARGV[2] 同理
KEYS[1] 具体是什么呢,看看this.getName()代码

//这个name是什么时候赋值的呢
  private final String name;
 public String getName() {
        return this.name;
    }
// 在final RLock lock = redisson.getLock(lockkey); 获取锁的时候  通过构造方法 把参数穿到name里面

因此KEYS[1] 就是 锁的key 在上述案例中就是lock:key_1001
同理再看ARGV的值 internalLockLeaseTime 和 getLockName(threadId)

protected long internalLockLeaseTime;
final UUID id;
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
       	//默认30秒
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    }
String getLockName(long threadId) {
        return this.id + ":" + threadId;
    }

ARGV[1] 就是默认的过期时间30秒 ARGV[2] 是当前uuid + 当前线程id。
那么上述lua脚本 的一个if 的意思是执行了hset KEYS[1] ARGV[2] 1 然后设置过期时间。成功以后返回nil 等价于 null
到此加锁逻辑结束了

锁续命逻辑

锁续命逻辑在这个方法里面 RedissonLock.this.scheduleExpirationRenewal(threadId); 里面

private void scheduleExpirationRenewal(final long threadId) {
        if (!expirationRenewalMap.containsKey(this.getEntryName())) {
        	//定时调度器类似于定时线程池
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                   //锁时长的修改的逻辑 就是执行了对应的lua脚本
                    RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.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(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
                    //锁时长修改成功后调用
                    future.addListener(new FutureListener<Boolean>() {
                        public void operationComplete(Future<Boolean> future) throws Exception {
                            RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                            if (!future.isSuccess()) {
                                RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
                            } else {
                            	//上面lua脚本成功后返回1 对应java就是true
                                if ((Boolean)future.getNow()) {
                                   	//递归调用 
                                    RedissonLock.this.scheduleExpirationRenewal(threadId);
                                }

                            }
                        }
                    });
                }
                //this.internalLockLeaseTime / 3L 上面方法的时间间隔 
                //默认internalLockLeaseTime = 30 秒 那么就是 10 秒执行一次
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            
            if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
                task.cancel();
            }

        }
    }

上述源码流程就是 10 秒后执行一次 task 任务,这个任务执行了lua脚本
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;"
先判断锁存不存在,然后重新设置过期时间
最后递归调用 相当于循环 达到每10秒 续命一次的过程

redisson获取不到锁自旋逻辑

回顾之前的加锁lua脚本

"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]);",

第一个if就是设置锁 如果成功 返回nil 就是java的null
第二个if 是锁重入的逻辑 如果成功 返回nil
如果上面两个if都没有成功,就是获取锁失败了 那么返回 锁的剩余过期时间

```java
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        //获取当前线程
        long threadId = Thread.currentThread().getId();
        //核心加锁逻辑 加锁成功后返回null  失败返回剩余锁的过期时间
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        if (ttl != null) {
        	//获取锁失败的线程  订阅加锁成功的线程
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);
            try {
            	//获取锁失败的自旋逻辑
                while(true) {
                	//再一次获取锁 得到锁的过期时间
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    if (ttl == null) {
                        //返回null 说明获取锁成功 退出自旋
                        return;
                    }
                    //如果过期时间 >= 0 就是阻塞 ttl的时间
                    if (ttl >= 0L) {
                    	//getLatch() 得到的其实是 java的 信号量
                        this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } else {
                    	//如果过期时间-1 表示永不过期 只能等待锁删除后 唤醒
                        this.getEntry(threadId).getLatch().acquire();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }
private final Semaphore latch = new Semaphore(0);
public Semaphore getLatch() {
        return this.latch;
    }

解锁逻辑

 protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
        "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;"
        +"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); "
        +"if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", 
        Arrays.asList(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
    }

还是执行lua脚本
第一个if 判断 是否存在锁 == 0 就是不存在 不存在的话 调用publish 发布一个解锁消息
第二个if 判断 锁存在的清苦下 这个锁是不是自己的 如果不是自己的就返回nil 如果是的话 调用hincrby 减一
第三个if 判断 减一以后这个锁的value 是不是等于0 因为锁重入的情况下 锁的value会累加
如果等于0 就删除这个锁 然后发布 解锁的消息

当其他线程通过订阅发布功能 接收到解锁的消息后会执行一个onMessage
在这里插入图片描述
就会唤醒 阻塞的线程。到此获取锁失败 阻塞 等待唤醒 就形成了一个闭环
那么到此redisson核心逻辑就结束了

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

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

相关文章

排序算法的实现

文章目录 一、排序的概念及其运用 1.排序的概念2.常见的排序算法二、常见排序算法的实现 1.插入排序 1.直接插入排序2.希尔排序2.选择排序 1.直接选择排序2.堆排序3.交换排序 1.冒泡排序2.快速排序 1.hoare版本2.挖坑法3.前后指针版本4.归并排序5.非比较排序三、排序算法复杂度…

AOSP 8.0 系统启动之一内核启动

目录 一、前言 二、涉及源码​​​​​​​ 三、源码分析​​​​​​​ 一、前言 Android本质上就是一个基于Linux内核的操作系统&#xff0c;与Ubuntu Linux、Fedora Linux类似&#xff0c;我们要讲Android&#xff0c;必定先要了解一些Linux内核的知识。 Linux内核的东西…

LeetCode - 630 课程表Ⅲ

目录 题目来源 题目描述 示例 提示 题目解析 算法源码 题目来源 630. 课程表 III - 力扣&#xff08;LeetCode&#xff09; 题目描述 这里有 n 门不同的在线课程&#xff0c;按从 1 到 n 编号。给你一个数组 courses &#xff0c;其中 courses[i] [durationi, lastDay…

被面试官问住了,MySQL两阶段提交是什么鬼?

前言 MySQL通过两阶段提交的机制&#xff0c;保证了redo log和bin log的逻辑一致性&#xff0c;进而保证了数据的不丢失以及主从库的数据一致。 而说起两阶段提交&#xff0c;就不得不先介绍一下redo log和bin log。 redo log redo log即重做日志&#xff0c;是InnoDB引擎特…

捷报频传 | 中睿天下再获“2022信创产业实干者企业”荣誉称号

近日&#xff0c;国内信创专业媒体“信创产业”正式公布“2022信创产业实干者”申报结果&#xff0c;同期发布信创产业实干者全景图。作为以“实战对抗”为特点的能力价值型网络安全厂商&#xff0c;中睿天下凭借自主研发实力、信创生态兼容性等综合实力&#xff0c;再次入选信…

stm32使用TB6600驱动器控制42BYGH型步进电机

stm32使用TB6600驱动器控制42BYGH型步进电机 stm32使用TB6600驱动器控制42BYGH型步进电机 文章目录stm32使用TB6600驱动器控制42BYGH型步进电机前言一、使用的设备说明介绍24V开关电源TB6600驱动器产品特点技术规格拨码开关设定42BYGH 步进电机接线方法控制步进电机的正反转控…

深度学习与排序模型发展

8.3 深度学习与排序模型发展 学习目标 目标 了解深度学习排序模型的发展应用 无 8.3.1 模型发展 CTR/CVR预估经历了从传统机器学习模型到深度学习模型的过渡。下面先简单介绍下传统机器学习模型&#xff08;GBDT、LR、到深度模型及应用&#xff0c;然后再详细介绍在深度学习…

基于遗传算法的配电网重构研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Android MVVM 开发新姿势 一

Android MVVM 开发新姿势 一 1. 前言 网上有不少关于Android架构的讨论&#xff0c;如MVC&#xff0c; MVP&#xff0c;MVVM&#xff0c;最近还有MVI&#xff0c;emmm…不得不感慨时代变化太快。MVVM出来也有一段很长的时间了&#xff0c;接触时间不长&#xff0c;写一篇文章…

快速入门pandas进行数据挖掘数据分析[多维度排序、数据筛选、分组计算、透视表](一)

1. 快速入门python&#xff0c;python基本语法 Python使用缩进(tab或者空格)来组织代码&#xff0c;而不是像其 他语言比如R、C、Java和Perl那样用大括号。考虑使用for循 环来实现排序算法: for x in list_values:if x < 10:small.append(x)else:bigger.append(x)标量类型 …

uni-app中vant-Weapp组件库的使用

先创建一个基础的uni-app目录从github下载vant包&#xff0c;zip格式的https://github.com/youzan/vant-weapp/releases项目根目录下创建wxcomponents文件夹把我们下好的文件vant-weapp里面只留下dist其余的可以全部删掉&#xff0c;然后把vant-weapp放到 wxcomponents里面App.…

在CentOS-6.9配置apache服务(2)---虚拟目录配置

文章目录一 需求二 系统环境三 基于Alias普通别名3.1 配置个人主页3.2 编写虚拟目录配置文件3.3 测试四 基于ScriptAlias脚本别名4.1 编写主配置文件4.2 创建测试主页4.3 测试一 需求 基于用户个人主页的身份验证&#xff0c;在浏览器输入 10.0.0.100/~a 可以得到用户a的个人网…

Linux:CPU频率调节模式以及降频方法简介

概述 cpufreq的核心功能&#xff0c;是通过调整CPU的电压和频率&#xff0c;来兼顾系统的性能和功耗。在不需要高性能时&#xff0c;降低电压和频率&#xff0c;以降低功耗&#xff1b;在需要高性能时&#xff0c;提高电压和频率&#xff0c;以提高性能。 cpufreq 是一个动态调…

拉伯证券|机构看好中国经济 人民币资产吸引力持续增强

2023年人民币汇率以及A股强势开局。1月以来人民币对美元中心价已累计增值超3%&#xff0c;接连3个月增值。到1月末&#xff0c;北向资金累计净买入额达1311.46亿元&#xff0c;刷新了沪深股通单月净买入新高。 在“真金白银”加仓布局人民币财物的一起&#xff0c;外资组织也纷…

CSS实现9宫格布局的4种方法:flex、float、grid、table布局

一、实现效果及html代码 1、实现效果 2、html代码 <body><div class"container"><div style"background-color: red">1</div><div style"background-color: blue">2</div><div style"background-…

十二、树结构的实际应用—赫夫曼树

1、赫夫曼树 1.1 基本介绍 给定 n 个权值作为 n 个叶子节点&#xff0c;构造一棵二叉树&#xff0c;若该树的带权路径长度&#xff08;wpl&#xff09;达到最小&#xff0c;称这样的二叉树为最优二叉树&#xff0c;也称哈夫曼树&#xff08;Huffman Tree&#xff09;&#xf…

Java工厂模式

定义&#xff1a;将创建对象的权利交给工厂类实现&#xff0c;解耦对象使用者和对象创建过程。 工厂模式有三种&#xff1a; 1、简单工厂模式 2、工厂方法模式 3、抽象工厂模式 使用工厂模式作用&#xff1a; 1、客户类和对象之间的耦合关系转移到了工厂方法和对象之间 …

pl/sql篇之變量的定義

簡述本篇文章主要介紹pl/sql的變量的簡單數據類型&#xff0c;複雜數據類型定義和調用方法&#xff0c;希望能對讀者有些許作用數據類型介紹變量的定義和調用在pl/sql中&#xff0c;定義的變量在聲明之後&#xff0c;可以直接在後續的sql調用&#xff0c;使用上非常方便簡單數據…

图解 MySQL MVCC 实现原理

文章目录MVCC 产生背景InnoDB 引擎表的隐藏列Undo 回滚版本链一致性视图MVCC 实现原理举例说明 MVCC 实现过程MVCC 产生背景 最早的数据库系统,只有读读之间可以并发,读写,写读,写写之间都要阻塞。而 MVCC (Muti Version Concurrency Control) , 是一种多版本并发控制机制。在…

Pandas+Pyecharts | 全国吃穿住行消费排行榜,最‘抠门’的地区居然是北京!!!

文章目录&#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 计算各项占比&#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 全国各地区人均收入、消费支出排行榜3.2 全国各地区人均可支配收入地图3.3 全国…