Redisson 分布式锁实战应用解析

文章目录

  • 前言
  • 一、Redisson介绍
  • 二、Redisson的使用
    • 1.1 引入依赖
    • 1.2 编写配置
    • 1.3 示例测试_01
    • 1.4 示例测试_02
  • 三、Redisson源码分析
    • 2.1 加锁源码
    • 2.2 看门狗机制


前言

分布式锁主要是解决分布式系统下数据一致性的问题。在单机的环境下,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 Java 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。

一、Redisson介绍

Redisson 是一个基于 Redis 的分布式 Java 客户端。它提供了丰富的功能和工具,帮助开发者在分布式系统中解决数据共享、并发控制和任务调度等问题。通过使用Redisson,开发者可以轻松地操作 Redis 的分布式对象(如集合、映射、队列等),实现可靠的分布式锁机制,以及管理和调度分布式环境中的任务和服务。

Redisson 的分布式锁的特点

● 线程安全:分布式锁可以确保在多线程和多进程环境下的数据一致性和可靠性。
● 可重入性: 同一个线程可以多次获取同一个锁,避免死锁的问题。
● 锁超时: 支持设置锁的有效期,防止锁被长时间占用而导致系统出现问题。
● 阻塞式获取锁: 当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程可以选择等待直到锁释放。
● 无阻塞式获取锁: 当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程不会等待,而是立即返回获取锁失败的信息。

redisson 实现分布式官网文档:https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

二、Redisson的使用

Redisson 支持单点模式、主从模式、哨兵模式、集群模式,本文以单点模式为例说明。

1.1 引入依赖

<!-- redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.12.0</version>
</dependency>
<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.1.21.RELEASE</version>
</dependency>

1.2 编写配置

spring:redis:host: 192.168.57.129port: 6379
@Configuration
public class RedissonConfig {@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private String port;/*** 对所有redisson的使用都是通过redissonClient对象* @return*/@Bean(destroyMethod = "shutdown")public RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://" + host + ":" + port);return Redisson.create(config);}
}

1.3 示例测试_01

@Autowired
private RedissonClient redissonClient;// redisson分布式锁的key
private static final String LOCK_TEST_KEY = "redisson_lock";// redisson分布式锁的key
private static int TICKET_NUMBER = 10;/*** 分布式锁测试用例* 模拟开启11个用户去抢车票*/
@Test
public void lockTest() {// 利用循环+多线程 模仿高并发请求for (int i = 0; i < 11; i++) {CompletableFuture.runAsync(() -> {if (TICKET_NUMBER > 0) {// 这里获取公平锁,遵循先进先出原则,方便测试RLock fairLock = redissonClient.getFairLock(LOCK_TEST_KEY);try {// 尝试加锁// waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败// leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)boolean lock = fairLock.tryLock(3000, 30, TimeUnit.MILLISECONDS);if (lock){log.info("线程:" + Thread.currentThread().getName() + "获得了锁");log.info("车票剩余数量:{}", --TICKET_NUMBER);}} catch (InterruptedException e) {e.printStackTrace();} finally {log.info("线程:" + Thread.currentThread().getName() + "准备释放锁");//注意: 无论出现任何情况都要主动解锁fairLock.unlock();}}else {log.info("车票已售罄!");}});try {// ->_-> 这里使当前方法占用的线程休息10秒,不要立即结束Thread.sleep(3000L);} catch (InterruptedException e) {e.printStackTrace();}}
}

日志信息输出

2023-11-18 15:27:00.834  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:00.835  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:9
2023-11-18 15:27:00.835  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:03.749  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:03.749  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:8
2023-11-18 15:27:03.749  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:06.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:06.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:7
2023-11-18 15:27:06.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:09.749  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:09.750  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:6
2023-11-18 15:27:09.750  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:12.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:12.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:5
2023-11-18 15:27:12.759  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:15.752  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:15.752  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:4
2023-11-18 15:27:15.752  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:18.762  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:18.762  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:3
2023-11-18 15:27:18.762  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:21.754  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:21.754  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:2
2023-11-18 15:27:21.754  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:24.763  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:24.763  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:1
2023-11-18 15:27:24.763  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:27.757  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6获得了锁
2023-11-18 15:27:27.757  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票剩余数量:0
2023-11-18 15:27:27.757  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 线程:ForkJoinPool.commonPool-worker-6准备释放锁
2023-11-18 15:27:30.753  INFO 5800 --- [onPool-worker-6] c.a.g.p.GulimallProductApplicationTests  : 车票已售罄!

1.4 示例测试_02

@ResponseBody
@GetMapping("/hello")
public String hello(){//1、获取一把锁,只要锁的名字一样,就是同一把锁String lockKey = "my-lock";RLock lock = redissonClient.getLock(lockKey);//2、加锁阻塞式等待,默认加的锁都是30s。lock.lock();//10秒自动解锁,自动解锁时间一定要大于业务的执行时间。问题:在锁时间到了以后,不会自动续期。//lock.lock(10, TimeUnit.SECONDS);//最佳实战:省掉了整个续期操作。手动解锁//1)、lock.lock(30, TimeUnit.SECONDS);try {log.info("加锁成功,执行业务ing, 线程ID = {}", Thread.currentThread().getId());Thread.sleep(10000);}catch (Exception e){e.printStackTrace();}finally {//3、解锁 假设解锁代码没有运行,redisson会不会出现死锁log.info("释放锁, 线程ID = {}", Thread.currentThread().getId());lock.unlock();}return "hello";
}

浏览器执行两个 hello 请求,只有当第一个请求业务执行完,第二个才能正常执行,不然第二个处于阻塞式等待状态。

在这里插入图片描述

控制台打印日志信息

2023-11-18 16:01:00.784  INFO 3916 --- [io-10000-exec-4] c.a.g.product.web.IndexController        : 加锁成功,执行业务ing, 线程ID = 116
2023-11-18 16:01:10.785  INFO 3916 --- [io-10000-exec-4] c.a.g.product.web.IndexController        : 释放锁, 线程ID = 116
2023-11-18 16:01:10.794  INFO 3916 --- [io-10000-exec-2] c.a.g.product.web.IndexController        : 加锁成功,执行业务ing, 线程ID = 114
2023-11-18 16:01:20.794  INFO 3916 --- [io-10000-exec-2] c.a.g.product.web.IndexController        : 释放锁, 线程ID = 114

redisson 实现分布式锁解决了 redis 实现分布式锁的两个问题

  1. 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉。
  2. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

三、Redisson源码分析

redisson 这个框架的实现依赖了 Lua 脚本和 Netty,以及各种 Future 及FutureListener 的异步、同步操作转换,加锁和解锁过程中还巧妙地利用了 redis 的发布订阅功能。

2.1 加锁源码

无参加锁方法

@Override
public void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}
}

有参加锁方法

@Override
public void lock(long leaseTime, TimeUnit unit) {try {lock(leaseTime, unit, false);} catch (InterruptedException e) {throw new IllegalStateException();}
}

2.2 看门狗机制

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {long threadId = Thread.currentThread().getId();//尝试获取锁Long ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return;}RFuture<RedissonLockEntry> future = subscribe(threadId);if (interruptibly) {commandExecutor.syncSubscriptionInterrupted(future);} else {commandExecutor.syncSubscription(future);}try {while (true) {ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {break;}// waiting for messageif (ttl >= 0) {try {future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {if (interruptibly) {future.getNow().getLatch().acquire();} else {future.getNow().getLatch().acquireUninterruptibly();}}}} finally {unsubscribe(future, threadId);}
}
//尝试获取锁
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {return get(tryAcquireAsync(leaseTime, unit, threadId));
}

异步的方式尝试获取锁

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1) {//如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间。return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}//如果我们未指定锁的超时时间,就使用30*1000【LockWatchdogTimeout看门狗的默认时间】RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);//占锁成功ttlRemainingFuture.onComplete((ttlRemaining, e) -> {//发生异常直接返回,若无异常执行下面逻辑if (e != null) {return;}// lock acquiredif (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}});return ttlRemainingFuture;
}
//默认自动续期时间30s,看门狗时间
private long lockWatchdogTimeout = 30 * 1000;
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {internalLockLeaseTime = unit.toMillis(leaseTime);return commandExecutor.evalWriteAsync(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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

重新设置超时时间

private void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);renewExpiration();}
}

开启定时任务,发送 LUA 脚本,锁的超时时间达到1/3就重新设为30s

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {// reschedule itselfrenewExpiration();}});}//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期,续成30s internalLockLeaseTime【看门狗时间】 / 3,10s}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

重新设置超时时间 LUA 脚本

protected RFuture<Boolean> renewExpirationAsync(long threadId) {return commandExecutor.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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));}

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

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

相关文章

【报错记录】解决使用Kotlin写的SpringBoot项目使用Aspect切面无法生效的问题

前言 为了能在SpringBoot使用Kotlin&#xff0c;真的是各种坑都彩礼一遍&#xff0c;这次遇到的问题是Aspect无法对Kotlin代码生效。我这里的使用场景是使用切面切Controller中的方法&#xff0c;用来对接口进行一些初始化和收尾工作。 Aspect在Controller类还是Java代码的时…

迁新址 启新程|美创科技杭州总部乔迁仪式圆满举行

“迁新址 启新程” 2023年11月21日 美创科技杭州总部乔迁仪式隆重举行 杭州未来科技城管委会、余杭国投集团、浙江省网络空间安全协会、浙江鸿程、华睿投资、金艮投资、如山资本、赛伯乐投资、宽带资本、普华投资、国中创投、密码资本、东方富海、之江商学、阿里云、联通&…

cc linux用root用户执行chmod 777 -R ./提示 Operation not permitted怎么办?

如果你作为 root 用户执行 chmod 777 -R ./ 命令时收到 “Operation not permitted” 错误&#xff0c;可能有几个原因&#xff1a; 不可更改 (Immutable) 文件属性&#xff1a; 文件可能被设置为不可更改。即使是 root 用户也不能修改这些文件的权限。使用 lsattr 命令查看文件…

Oracle与Redis Enterprise协同,作为企业缓存解决方案

来源&#xff1a;虹科云科技 虹科干货丨Oracle与Redis Enterprise协同&#xff0c;作为企业缓存解决方案 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 单独使用Oracle作为企业缓存数据库时&#xff0c;会出现哪些问题呢&#xff1f;使用Redis Enterprise与Oracle共…

使用 Filebeat+Easysearch+Console 打造日志管理平台

近年来&#xff0c;日志管理平台越来越流行。使用日志管理平台可以实时地、统一地、方便地管理和查看日志&#xff0c;挖掘日志数据价值&#xff0c;驱动运维、运营&#xff0c;提升服务管理效率。 方案架构 Beats 是轻量级采集器&#xff0c;包括 Filebeat、Metricbeat 等。E…

基于vue的three.js学习

vue安装three.js&#xff0c;并创建第一个入门场景 安装three.js npm install --save three引入three.js import * as THREE from threethree.js结构 three.js坐标 创建一个场景 scene场景&#xff0c;camera相机&#xff0c;renderer渲染器 创建一个场景 this.scene new T…

这7款神仙软件,程序员必备!

如果你是程序员、开发者、网络运维等 IT 从业者日常工作中大家肯定会用到很多网站&#xff0c;今天给大家带来7款压箱底的神仙软件&#xff0c;希望可以帮助有需要的码农朋友实现更高效地办公。 一、Everything 适用&#xff1a;本地文件搜索神器 就是为了极速检索而生,其实…

yapi==使用依赖包里的类作为入参/返回值导出后没有备注

比如模块A中有个MyDemoEntity类&#xff0c;在B中以依赖的形式引入了A&#xff0c;并在B的接口中以MyDemoEntity作为返回值&#xff0c;导出到YAPI发现MyDemoEntity的备注没了。 解决&#xff1a; 将A的内容安装到本地MAVEN仓库&#xff0c;并且需要将源码也一起安装 <buil…

探索锦食送如何通过API集成无代码开发技术提高电商平台和营销系统效率

探索锦食送无代码开发集成技术 随着电子商务和营销系统的快速发展&#xff0c;企业不断寻求更高效和灵活的管理方式。锦食送&#xff0c;作为高端餐饮外卖服务的领先者&#xff0c;通过无代码开发的API集成技术&#xff0c;实现了电商平台和营销系统的高效管理。这种创新的连接…

# 学习 Prolog 和 离散逻辑的16个等价公式:一趟有趣的逻辑之旅

Prolog 的语法很奇怪,需要一些时间来适应,所以我花了点时间,想用Prolot来学习和验证离散逻辑的16组等价公式。 1. 双重否定律 (Double Negation Law) A ⇔A 首先&#xff0c;我们来看看双重否定律。在 Prolog 中&#xff0c;我们可以这样验证它&#xff1a; fun1(A,Z):-memb…

干货分享:本地生活服务商入驻申请需要哪些条件?附更快捷的方法!

在数字化时代&#xff0c;本地服务市场已经成为各大平台争夺的重要阵地。抖音不仅在短视频市场占有一席之地&#xff0c;同时也在如火如荼的开发着本地服务市场&#xff0c;相继支付宝、视频号也推出了本地生活服务商模式。本文将介绍抖音本地生活服务商的申请条件&#xff0c;…

dedecms标签

【Arclist 标记】这个标记是DedeCms最常用的一个标记&#xff0c;也叫自由列表标记&#xff0c;其中 hotart、coolart、likeart、artlist、imglist、imginfolist、specart、autolist 这些标记都是由这个标记所定义的不同属性延伸出来的别名标记。功能说明&#xff1a;获取指定的…

WIN10 WIN11 12代 13代 大小核;性能核;电源选项;P-core,E-core;

WIN10 WIN11 12代 13代 大小核&#xff1b;性能核&#xff1b;电源选项&#xff1b;P-core,E-core&#xff1b; WIN10 WIN11 12代 13代 大小核;性能核&#xff1b;电源选项&#xff1b;P-core,E-core;说明&#xff1a;方法&#xff1a;1。右键开始菜单&#xff0c;选择“Window…

时序预测 | MATLAB实现基于ELM-AdaBoost极限学习机结合AdaBoost时间序列预测

时序预测 | MATLAB实现基于ELM-AdaBoost极限学习机结合AdaBoost时间序列预测 目录 时序预测 | MATLAB实现基于ELM-AdaBoost极限学习机结合AdaBoost时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.Matlab实现ELM-Adaboost时间序列预测&#xff0c;极…

Django 集成 Celery 实现高效的异步任务处理

概要 在复杂的 Web 应用中&#xff0c;处理长时间运行的任务或定期任务是一项挑战。Django 作为一个强大的 Python Web 框架&#xff0c;可以通过集成 Celery 这一异步任务队列来优化这些任务的处理。Celery 不仅能提高应用性能&#xff0c;还能改善用户体验。本文将深入探讨如…

理解系统内核linux phy驱动

PHY设备驱动是基于device、driver、bus的连接方式,驱动涉及如下几个重要部分&#xff1a; 总线 - sturct mii_bus (mii stand for media independent interface) 设备 - struct phy_device 驱动 - struct phy_driver。PHY驱动函数驱动功能&#xff1a; 函数名称 功能描述 s…

arcgis中投影文件(.prj)和地理转换文件(.gtf)存储路径

1、投影文件&#xff08;自定义的.prj&#xff09;的存储路径 C:\Users\14635\AppData\Roaming\ESRI\Desktop10.5\ArcMap\Coordinate Systems 2、地理转换文件&#xff08;.gtf&#xff09;--自定义 C:\Users\14635\AppData\Roaming\ESRI\Desktop10.5\ArcToolbox\CustomTransfo…

Android Studio App开发实战项目之实现淘宝电商App首页界面(附源码,可用于大作业参考)

需要源码和图片集请点赞关注收藏后评论区留言或者私信~~~ 各家电商的App首页都是动感十足&#xff0c;页面元素丰富令人眼花缭乱&#xff0c;其中运用了Android的多种组合控件&#xff0c;可以说是App界面开发的集大成之作&#xff0c;下面我们也动手实现一个。 一、需求描述 …

2022年12月 电子学会青少年软件编程 中小学生Python编程 等级考试一级真题答案解析(选择题)

2022年12月Python编程等级考试一级真题解析 选择题(共25题,每题2分,共50分) 1、关于Python语言的注释,以下选项中描述错误的是 A、Python语言有两种注释方式:单行注释和多行注释 B、Python语言的单行注释以#开头 C、Python多行注释使用###来做为标记 D、注释用于解…