Spring声明式事务

devtools/2025/1/18 8:08:07/

1. 前言

        在上一篇博客中从一个案例 静态代理 -> 动态代理 -> AOP-CSDN博客 介绍了静态代理 -> 动态代理 -> SpringAOP相关内容。在Spring中声明式事务的底层就是通过AOP来实现的。趁热打铁,本篇博客介绍Spring的事务相关内容。

        在此之前,我们先来说明下什么是声明式编程。和声明式编程对应的是命令式编程。凡是非命令式编程都可归结为声明式编程。编程范式可分为两大类:

  • 命令式编程(Imperative Programming)
  • 声明式编程(Declarative Programming)
    • 函数式编程(Functional Programming,简称FP)
    • 逻辑式编程(Logic Programming,简称LP)
    • 属性式编程

其中命令式、函数式和逻辑式是最核心的三范式。这里引入网上看的一个图片,具体出处忘了是哪里。

在我们实际开发过程中,命令式编程和声明式编程的优缺点简单总结如下:

命令式编程

  • 优点:代码调试(Debug)容易
  • 缺点:代码量大,需要自己手动编写大量代码

声明式编程

  • 优点:代码简洁,使用方便
  • 缺点:封装太深,不易调试(Debug);  

2. 项目需求概述

        通过一个账户表和一个图书表来演示事务的作用。

2.1 SQL脚本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '用户id',`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',`age` int(0) NULL DEFAULT NULL COMMENT '年龄',`balance` decimal(10, 2)  NULL DEFAULT NULL COMMENT '余额',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES (1, 'zhangsan', 18, 10000.00);
INSERT INTO `account` VALUES (2, 'lisi', 20, 10000.00);
INSERT INTO `account` VALUES (3, 'wangwu', 16, 10000.00);-- ----------------------------
-- Table structure for book
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book`  (`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '图书id',`bookName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图书名',`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '单价',`stock` int(0) NULL DEFAULT NULL COMMENT '库存量',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO `book` VALUES (1, '剑指Java', 100.00, 100);
INSERT INTO `book` VALUES (2, '剑指大数据', 100.00, 100);
INSERT INTO `book` VALUES (3, '剑指Offer', 100.00, 100);SET FOREIGN_KEY_CHECKS = 1;

2.2. 引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>

2.3. 项目目录结构及代码

Account

java">import lombok.Data;import java.math.BigDecimal;@Data
public class Account {private Integer id;private String userName;private Integer age;private BigDecimal balance;
}

Book

java">import lombok.Data;import java.math.BigDecimal;@Data
public class Book {private Integer id;private String bookName;private BigDecimal price;private Integer stock;
}

AccountDao

java">import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;import java.math.BigDecimal;@Component
public class AccountDao {private final JdbcTemplate jdbcTemplate;public AccountDao(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** 按照userName扣减账户余额** @param userName 用户名* @param money    要扣减的金额*/public void updateBalanceByUserName(String userName, BigDecimal money) {String sql = "update account set balance = account.balance - ? where username = ?";jdbcTemplate.update(sql, money, userName);}
}

BookDao

java">import com.shg.spring.tx.bean.Book;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Component
public class BookDao {private final JdbcTemplate jdbcTemplate;public BookDao(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** 按照图书id删除图书** @param bookId 图书id*/public void deleteBookById(int bookId) {String sql = "delete from book where id=?";jdbcTemplate.update(sql, bookId);}/*** 更新图书库存** @param bookId 图书id* @param num    需要减去的库存数量*/public void updateBookStockById(int bookId, Integer num) {String sql = "update book set stock=stock - ? where id=?";jdbcTemplate.update(sql, num, bookId);}/*** 新增一个图书** @param book*/public void addBook(Book book) {String sql = "insert into book (bookName,price,stock) values (?, ?, ?)";jdbcTemplate.update(sql, book.getBookName(), book.getPrice(), book.getStock());}/*** 根据id查询书籍信息** @param id 图书id* @return Book*/@Transactionalpublic Book getBookById(Integer id) {String sql = "select * from book where id = ?";return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);}}

UserService

java">import java.io.IOException;public interface UserService {void checkout(String userName, Integer bookId, int buyNum);
}

UserServiceImpl

java">import com.shg.spring.tx.bean.Book;
import com.shg.spring.tx.dao.AccountDao;
import com.shg.spring.tx.dao.BookDao;
import com.shg.spring.tx.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.io.IOException;
import java.math.BigDecimal;@Service
public class UserServiceImpl implements UserService {private final BookDao bookDao;private final AccountDao accountDao;public UserServiceImpl(BookDao bookDao, AccountDao accountDao) {this.bookDao = bookDao;this.accountDao = accountDao;}@Overridepublic void checkout(String userName, Integer bookId, int buyNum) {// 1. 查询图书信息Book bookById = bookDao.getBookById(bookId);// 2. 计算总价BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));// 3. 扣减库存bookDao.updateBookStockById(bookId, buyNum);// 4. 扣减余额accountDao.updateBalanceByUserName(userName, totalPrice);}
}

3. Spring事务案例

3.1. 没有事务的情况

        在上述的UserServiceImp代码中,理想情况下代码执行后,图书book表 和 账户account表都表现正常。即图书库存减少,账户余额扣减成功。但是如果在执行结账(checkout)方法时,抛出了异常,理论上对数据库做的操作都应该回滚。但实际上数据库并没有回滚。所以默认情况下的业务逻辑是没有事务控制的。

3.2. 使用Spring声明式事务

(1)在启动类上标注 @EnableTransactionManagement注解;

(2)在需要事务的方法上标注 @Transactional

启动类

java">import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;@EnableTransactionManagement
@SpringBootApplication
public class Spring03TxApplication {public static void main(String[] args) {SpringApplication.run(Spring03TxApplication.class, args);}}

事务方法

java">    @Transactional@Overridepublic void checkout(String userName, Integer bookId, int buyNum) throws InterruptedException, IOException {// 1. 查询图书信息Book bookById = bookDao.getBookById(bookId);// 2. 计算总价BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));// 3. 扣减库存bookDao.updateBookStockById(bookId, buyNum);// 4. 扣减余额accountDao.updateBalanceByUserName(userName, totalPrice);// 5. 模拟异常回滚int i = 1 / 0;}

3.3. Spring声明式事务的底层原理

(1)事务管理器 TransactionManager:控制事务的提交和回滚

(2)事务拦截器 TransactionInterceptor:控制事务何时提交和回滚

在TransactionInterceptor中有一个调用目标方法的逻辑:

进入 invokeWithTransaction方法,进入到TransactionAspectSupport类中,源码核心方法如下:

java">	/*** General delegate for around-advice-based subclasses, delegating to several other template* methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}* as well as regular {@link PlatformTransactionManager} implementations and* {@link ReactiveTransactionManager} implementations for reactive return types.* @param method the Method being invoked* @param targetClass the target class that we're invoking the method on* @param invocation the callback to use for proceeding with the target invocation* @return the return value of the method, if any* @throws Throwable propagated from the target invocation*/@Nullableprotected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {// If the transaction attribute is null, the method is non-transactional.TransactionAttributeSource tas = getTransactionAttributeSource();final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);final TransactionManager tm = determineTransactionManager(txAttr);if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);boolean hasSuspendingFlowReturnType = isSuspendingFunction &&COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {Class<?> reactiveType =(isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);if (adapter == null) {throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +method.getReturnType() + "] with specified transaction manager: " + tm);}return new ReactiveTransactionSupport(adapter);});return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);}PlatformTransactionManager ptm = asPlatformTransactionManager(tm);final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {// Standard transaction demarcation with getTransaction and commit/rollback calls.TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);Object retVal;try {// This is an around advice: Invoke the next interceptor in the chain.// This will normally result in a target object being invoked.retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// target invocation exceptioncompleteTransactionAfterThrowing(txInfo, ex);throw ex;}finally {cleanupTransactionInfo(txInfo);}if (retVal != null && txAttr != null) {TransactionStatus status = txInfo.getTransactionStatus();if (status != null) {if (retVal instanceof Future<?> future && future.isDone()) {try {future.get();}catch (ExecutionException ex) {if (txAttr.rollbackOn(ex.getCause())) {status.setRollbackOnly();}}catch (InterruptedException ex) {Thread.currentThread().interrupt();}}else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {// Set rollback-only in case of Vavr failure matching our rollback rules...retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);}}}commitTransactionAfterReturning(txInfo);return retVal;}else {Object result;final ThrowableHolder throwableHolder = new ThrowableHolder();// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.try {result = cpptm.execute(txAttr, status -> {TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);try {Object retVal = invocation.proceedWithInvocation();if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {// Set rollback-only in case of Vavr failure matching our rollback rules...retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);}return retVal;}catch (Throwable ex) {if (txAttr.rollbackOn(ex)) {// A RuntimeException: will lead to a rollback.if (ex instanceof RuntimeException runtimeException) {throw runtimeException;}else {throw new ThrowableHolderException(ex);}}else {// A normal return value: will lead to a commit.throwableHolder.throwable = ex;return null;}}finally {cleanupTransactionInfo(txInfo);}});}catch (ThrowableHolderException ex) {throw ex.getCause();}catch (TransactionSystemException ex2) {if (throwableHolder.throwable != null) {logger.error("Application exception overridden by commit exception", throwableHolder.throwable);ex2.initApplicationException(throwableHolder.throwable);}throw ex2;}catch (Throwable ex2) {if (throwableHolder.throwable != null) {logger.error("Application exception overridden by commit exception", throwableHolder.throwable);}throw ex2;}// Check result state: It might indicate a Throwable to rethrow.if (throwableHolder.throwable != null) {throw throwableHolder.throwable;}return result;}}

上面这段源码的重点:

completeTransactionAfterThrowing(txInfo, ex); 【异常通知】出现异常时回滚
commitTransactionAfterReturning(txInfo); 【返回通知】方法正常执行后,提交事务

通常我们不会自己去实现事务管理器的接口,而是使用Spring给我们提供的事务管理器。Spring声明式事务的底层原理就是:

Spring底层通过 事务管理器 和 事务拦截器 实现Spring的声明式事务。具体来说就是:

(1)事务管理器的顶层接口是 TransactionManage,其子接口中定义了获取事务getTransaction方法、提交事务方法和回滚事务方法。控制事务的提交和回滚。我们默认是使用JdbcTransactionManage这个实现类。


(2)事务拦截器:是一个切面,如果目标方法正常执行,就会调用事务管理器的提交事务方法;如果目标方法执行出现异常,就会调用事务管理器的回滚事务方法。

3.4. 事务注解@Transaction的属性介绍

在@Transactional注解中有许多可以设置的属性,如下图:

下面针对这些属性进行介绍。 

3.4.1. value 或 transactionManager属性

用来设置要使用的事务管理器,通常我们不指定,而是使用Spring提供的默认的事务管理器。如下图

3.4.2. label属性 

通常不使用,不介绍  

 3.4.3. propagation属性 

先理解什么是事务的传播行为:当一个大的事务方法里面嵌套小的事务方法时,该如何控制大事物和小事物的关系。比如一个结账方法是一个大事物,此方法内部调用了两个事务方法:扣减库存方法 和 扣减余额方法。伪代码如下:

大事物:结账方法
@Transaction

public void checkout(String userName, Integer bookId, int buyNum) {

        // 调用扣减库存方法

        bookDao.updateBookStockById(bookId, buyNum);

        

        // 调用扣减余额方法

        accountDao.updateBalanceByUserName(userName, totalPrice);

}

小事物:扣减库存方法

@Transactional
public void updateBookStockById(int bookId, Integer num) {String sql = "update book set stock=stock - ? where id=?";jdbcTemplate.update(sql, num, bookId);
}

小事物:扣减余额方法

@Transactional
public void updateBalanceByUserName(String userName, BigDecimal money) {String sql = "update account set balance = account.balance - ? where username = ?";jdbcTemplate.update(sql, money, userName);
}

有了上面的调用逻辑,当执行结账方法时,已经开启了一个事务;那么结账方法调用扣减库存或扣减余额方法时,扣减库存(后面直接以扣减库存为例)方法该如何 “使用事务”呢?


此时就需要传播行为这个属性进行控制了。
 

控制事务的传播行为有如下七种:

(1)propagation = Propagation.REQUIRED:支持当前事务,如果不存在,则创建一个新事务。
【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法有事务,我就用你的事务;如果你没有事务我就自己新创建一个事务。

(2)propagation = Propagation.SUPPORTS:支持当前事务,如果不存在,则执行非事务。

【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法有事务,我就用你的事务;如果你没有事务我就以非事务方式运行。


(3)propagation = Propagation.MANDATORY:支持当前事务,如果不存在则抛出异常。
【解释】指的是内层的方法(updateBookStockById方法)支持当前外面这个方法(checkout方法)的事务。如果当前外面这个方法没有事务,则抛出异常。

(4)propagation = Propagation.REQUIRES_NEW:创建一个新事务,并挂起当前事务(如果存在)。
【解释】指的是内层的方法(updateBookStockById方法)会创建一个新事物,并在新事务里面执行。会挂起外面这个方法(checkout方法)的事务(如果外面方法存在事务)。

(5)propagation = Propagation.NOT_SUPPORTED:非事务性地执行,挂起当前事务(如果存在)

【解释】指的是内层的方法(updateBookStockById方法)以非事务的方式执行,如果当前外面这个方法(checkout方法)有事务,则挂起当前事务。

(6)propagation = Propagation.NEVER:非事务执行,如果存在事务则抛出异常。

【解释】指的是内层的方法(updateBookStockById方法)必须以非事务的方式执行,如果当前外面这个方法有事务,则抛出异常。

(7)paopagation = Propagation.NESTED:如果当前事务存在,则在嵌套事务中执行,否则表现为REQUIRED。

【解释】指的是内层的事务方法(updateBookStockById方法)在执行时,判断当前外面的方法(checkout方法)是否有事务,如果有,就在外面这个方法的事务中再开启一个事务进行执行。如果外面的方法没有事务,则表现和 propagation = Propagation.REQUIRED 一样。

一张图总结:



总结来说:就是当大事物存在时,里面的小事物要不要和大事物进行绑定(和大事物的共生关系)
 

【属性传播】
注意:
当内层的小事物和外层的大事物共用一个事务,内层小事物的其他一些属性就都失效了(使用外层大事物属性)。比如timeout属性,readOnly属性,isolation属性等。

3.4.4. isolation属性 

isolation可以设置如下四种属性:

(1)@Transactional(isolation = Isolation.READ_UNCOMMITTED)

(2)@Transactional(isolation = Isolation.READ_COMMITTED)

(3)@Transactional(isolation = Isolation.REPEATABLE_READ)
(4)@Transactional(isolation = Isolation.SERIALIZABLE)

MySQL的默认隔离级别是可重复读,Oracle的默认隔离级别是读已提交。在实际开发中,隔离级别通常从这两个中间选一个。一般使用默认的。


设置事务的隔离级别,针对关系型数据库的事务特性和隔离级别,可以参考我之前写的一篇博文:事务的特性和隔离级别-CSDN博客

(1)隔离级别的目的是,当多个读写事务并发执行的时候,防止出现的脏读、不可重复读和幻读等情况的发生。

(2)不同的隔离级别,其可以解决的问题如下表所示:

隔离级别
级别/问题脏读不可重复读幻读
读未提交
读已提交×
可重复读(快照读)××
串行化×××

(3)思考:为什么隔离级别叫 未提交、已提交、可重复读。而不是未提交、已提交和可重复呢?这是因为隔离级别是控制 的。为啥控制读呢?

       
这是因为数据库底层在针对写(更新操作)时,会对其进行加锁,即使有并发写(并发修改)操作,也是要一个一个排队去执行更新操作。所以不会有并发问题。所以数据库的写操作会比较慢。

如果只是并发读,也不会出现并发问题,因为数据没有改变,读多少次数据都是一样的。

所以并发问题会出现在同时存在读写并发的场景。所以说读写一旦并发的时候,就需要有一种机制来控制有一个人在写时,控制这个读的人,应该何时可以读(而不用控制其他写的人,因为写操作,数据库本身就会加锁)。

(a)如果写的人,写了一半就可以让另一个人去读到还未写完的数据。这就是读未提交

(b)如果写的人,只有写完了,才可以让另一个人去读到刚才写入的数据。这就是读已提交

(c)如果写的人,写完了,事务也提交了,但是另一个读的人,再次读取还是和自己之前读取的数据一样,即:没有读到刚才那个人已经写入的数据。这就是可重复读。

3.4.5. timeout 或 timeoutString属性 

控制事务的超时时间(以秒为单位),一旦超过约定时间,事务就会回滚。

注意:事务的超时时间是指从方法开始,到最后一次数据库操作结束经过的时间。代码示例如下:

java">    @Transactional(timeout = 3)@Overridepublic void checkout(String userName, Integer bookId, int buyNum) throws InterruptedException, IOException {// 1. 查询图书信息Book bookById = bookDao.getBookById(bookId);// 2. 计算总价BigDecimal totalPrice = bookById.getPrice().multiply(new BigDecimal(buyNum));// 3. 扣减库存bookDao.updateBookStockById(bookId, buyNum);// 模拟事务超时//Thread.sleep(3000);// 4. 扣减余额(这是事务方法最后一次执行数据库操作,事务执行时间是以这次操作执行完成进行耗时统计的)accountDao.updateBalanceByUserName(userName, totalPrice);// 如果在这里进行睡眠(模拟业务耗时操作),则Spring事务不会统计超时。Thread.sleep(3000);}

3.4.6. readOnly属性 

如果整个事务都只是一个读操作,则可以把readOnly设置成true,可以实现底层运行时优化。 总结就是:readOnly可以做到只读优化

3.4.7. rollbackFor 或 rollbackForClassName属性 

(1)指明哪些异常出现时,事务进行回滚。默认并不是所有异常都一定进行回滚。

(2)默认运行时异常是可以进行回滚的,即Error和RuntimeException及其子类异常可以进行回滚。而编译时异常(也叫已检查异常)(除了运行时异常,其他都是编译时异常)默认是不回滚的。

(3)如果我们设置了rollbackFor属性,那么可以回滚的异常就是 运行时异常 + rollbackFor指定的异常。

(4)通常在实际业务中,我们都设置rollbackFor={Exception.class}

 

3.4.8. noRollbackFor 或 noRollbackForClassName 属性 

指明哪些异常不需要回滚,默认不回滚的异常是:编译时异常+noRollbackFor指定的异常

4. 如果此篇文章对你有帮助,感谢关注并点个赞~ 后续持续输出高质量的原创技术文章~


http://www.ppmy.cn/devtools/151507.html

相关文章

基于 Electron 应用的安全测试基础 — 提取和分析 .asar 文件

视频教程在我主页简介或专栏里 目录&#xff1a; 提取和分析 .asar 文件 4.1. .asar 文件提取工具 4.1.1. 为什么选择 NPX&#xff1f; 4.2. 提取过程 4.3. 提取 .asar 文件的重要性 4.3.1 关键词 4.3.2 执行关键词搜索 4.3.2.1 使用命令行工具“grep”进行关键词搜索 4.3.2…

【数据分析】coco格式数据生成yolo数据可视化

yolo的数据可视化很详细&#xff0c;coco格式没有。所以写了一个接口。 输入&#xff1a;coco格式的instances.json 输出&#xff1a;生成像yolo那样的标注文件统计并可视化 import os import random import numpy as np import pandas as pd import matplotlib import matplot…

几个Linux系统安装体验(续): 中标麒麟服务器系统

本文介绍中标麒麟服务器系统&#xff08;NeoKylin&#xff09;的安装。 下载 下载地址&#xff1a; https://product.kylinos.cn/productCase/42/25 下载文件&#xff1a;本文下载文件名称为NeoKylin-Server7.0-Release-Build09.06-20220311-X86_64.iso。 下载注意事项&…

剑指Offer|LCR 031. LRU 缓存

LCR 031. LRU 缓存 运用所掌握的数据结构&#xff0c;设计和实现一个 LRU (Least Recently Used&#xff0c;最近最少使用) 缓存机制 。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存…

Python贪心

贪心 贪心&#xff1a;把整体问题分解成多个步骤&#xff0c;在每个步骤都选取当前步骤的最优方案&#xff0c;直至所有步骤结束&#xff1b;每个步骤不会影响后续步骤核心性质&#xff1a;每次采用局部最优&#xff0c;最终结果就是全局最优如果题目满足上述核心性质&#xf…

【算法学习笔记】32:筛法求解欧拉函数

上节学习的是求一个数 n n n的欧拉函数&#xff0c;因为用的试除法&#xff0c;所以时间复杂度是 O ( n ) O(\sqrt{n}) O(n ​)&#xff0c;如果要求 m m m个数的欧拉函数&#xff0c;那么就会花 O ( m n ) O(m \sqrt{n}) O(mn ​)的时间。如果是求连续一批数的欧拉函数&#x…

DNS服务学习

DNS服务 一、什么是DNS服务二、概念三、疑问四、内容五、应用实践 一、什么是DNS服务 二、概念 三、疑问 四、内容 五、应用实践 windows server 2019 搭建dns服务器

浅谈云计算20 | OpenStack管理模块(下)

OpenStack管理模块&#xff08;下&#xff09; 五、存储管理5.1 存储管理概述 5.2 架构设计5.2.1 Cinder块存储架构5.2.2 Swift对象存储架构 六、网络管理6.1 网络管理概述6.2 架构解析6.2.1 Neutron网络服务架构6.2.2 网络拓扑架构 6.3 原理与流程6.3.1 网络创建原理6.3.2 网络…