【EF Core】 EF Core并发控制:乐观锁与悲观锁的应用
文章目录
- 前言
- 一、并发的风险
- 二、EF Core中的并发控制方式
- 2.1 开放式并发(乐观锁)
- 2.1.1 应用程序管理的属性并发令牌
- 2.1.2 数据库生成的并发令牌
- 2.2 悲观锁
- 总结
前言
实际的生产环境中,我们经常能遇到数据库由多个应用程序同时使用。每个程序作为一个实例都会对数据进行操作。其中如果不同的实例同时对同一个数据进行了修改,这就会遇到我们常见的一个问题——并发。
这里讨论的并发控制,也就是数据库管理系统中用于处理多个事务同时访问和修改共享数据时可能产生问题的机制。其主要目标是确保数据的一致性、完整性和隔离性,同时提高系统的性能和吞吐量。
一、并发的风险
前面我们了解到并发情况下,多个操作同时修改共享资源,容易导致 数据不一致或逻辑错误。
比如银行转账,用户 A 转账给用户 B,用户C也转账给用户B。假设用户 B 初始余额为 100 元,A 转 50 元,C 转 30 元。若两个转账操作并发执行,可能出现以下问题:
两个事务同时读取用户 B 的余额为 100 元。
事务 A 将余额更新为 150 元并提交。
事务 C 将余额更新为 130 元并提交,覆盖事务 A 的更新,最终余额变为 130 元(而非正确的 180 元)。
这就是多个操作同时修改共享资源时,可能导致 数据不一致的问题。是一个典型的丢失更新的例子
除了并发会导致丢失更新外,还有以下几种情况:
- 脏读(Dirty Read):一个事务读取了另一个未提交事务修改的数据。
- 不可重复读(Non-Repeatable Read):同一事务中多次读取同一数据却得到不同结果,因为其他事务已提交了修改。
- 幻读(Phantom Read):一个事务按相同条件多次查询,却返回了不同的记录集,原因是其他事务插入或删除了数据。
- 丢失更新(Lost Update):两个事务同时修改同一数据,后提交的事务覆盖了先提交事务的修改,导致部分修改丢失。
并发编程中处理并发冲突的两种不同策略,悲观锁与乐观锁。
悲观锁:假设冲突一定会发生,因此在操作数据前先获取锁,确保同一时间只有自己能修改数据,其他事务需等待锁释放。【比如银行转账】
乐观锁:假设冲突很少发生,不预先加锁,而是在提交更新时检查数据是否被其他事务修改过
二、EF Core中的并发控制方式
EFCore开发团队实现了乐观并发并集成在了EF Core里,这也是 EF Core 处理并发最常用的方式。乐观并发不会在数据库层面把数据进行锁定,但如果数据在查询后发生更改,该数据修改后会在保存时失败。
在 EF Core 中,乐观并发的实现是通过给属性设置一个并发令牌。 该并发令牌会在查询实体时进行加载和跟踪,和数据的其他属性一样。 然后,在 SaveChanges() 期间执行更新或删除操作时,数据库中的并发令牌值将与 EF Core 读取的原始值进行比较。
当然我们也可以监测多个属性值,把一整条数据的任何变化作为监测的对象,是一条数据的并发令牌。比起前者单个属性来说,行的并发令牌需要显示设置属性字段。这个我们后续一一讨论。
2.1 开放式并发(乐观锁)
前面我们了解到EFCore开发团队实现了乐观并发并集成在了EF Core里,设置一个并发令牌,保存的时候通过DbUpdateConcurrencyException来捕获异常,从而判断值是否已经被修改。对于单个属性和多个属性的检测,EFCore分别提供ConcurrencyToken和RowVersion两者方式来生成令牌。
- ConcurrencyToken是应用程序管理的并发令牌
- RowVersion是本机数据库生成的并发令牌
下面分别介绍二者
2.1.1 应用程序管理的属性并发令牌
设想对站点信息的修改的一个场景,两个实例修改同一个站点的Gis号。
- 属性值设定,指定的属性后面添加IsConcurrencyToken
builder.Property(s => s.GisNo).IsConcurrencyToken();
- 通过DbUpdateConcurrencyException捕获属性值并发修改异常。其中var databaseValues = await entry.GetDatabaseValuesAsync()可用于获取当前实体的数据库值。
catch (DbUpdateConcurrencyException ex)
{foreach (var entry in ex.Entries){if (entry.Entity is Station){//获取当前实体的数据库值var databaseValues = await entry.GetDatabaseValuesAsync();string newGisNo = databaseValues.GetValue<string>(nameof(Station.GisNo));Console.WriteLine($"并发冲突,有新{newGisNo}更新");}else{throw new NotSupportedException("Don't know how to handle concurrency conflicts for "+ entry.Metadata.Name);}}
}
完整代码
public class GisChange
{public async Task ExecuteAsync(){Console.WriteLine("===============开始修改GIS===================");Console.WriteLine("请输入GIS:");string gisNo = Console.ReadLine();Console.WriteLine("请确认是否继续:");Console.WriteLine("Y/N");string input = Console.ReadLine();if (input != "Y"){await ChangeAsync(gisNo, null);}else{await ChangeAsync(gisNo, () =>{Thread.Sleep(5000);});}}private async Task ChangeAsync(string gisNo,Action sleep){using (ApplicationDbContext dbContext = new ApplicationDbContext()){bool saved = false;while (!saved){try{Station? station = await dbContext.Stations.FindAsync(1);station.GisNo = gisNo;if(sleep != null) {sleep();}await dbContext.SaveChangesAsync();saved = true;Console.WriteLine("修改成功");}catch (DbUpdateConcurrencyException ex){foreach (var entry in ex.Entries){if (entry.Entity is Station){//获取当前实体的数据库值var databaseValues = await entry.GetDatabaseValuesAsync();string newGisNo = databaseValues.GetValue<string>(nameof(Station.GisNo));Console.WriteLine($"并发冲突,有新{newGisNo}更新");}else{throw new NotSupportedException("Don't know how to handle concurrency conflicts for "+ entry.Metadata.Name);}}}}}}
}
接下来我们启动两个控制台程序,几乎同时更新Gis编号。
监测到并发冲突
但是如果两次修改的是同一个值,在给属性颁发令牌的这种方式里,EF Core会忽略掉并发冲突
2.1.2 数据库生成的并发令牌
SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为RowVersion类型。对于RowVersion类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
在SQLServer中,timeStamp和rowVersion是同一种类型的不同别名而已
并不是所有数据库都支持自动生成rowVersion,因此每当持久保留更改时,都必须在应用程序中分配此属性。也可也实现对于行数据的并发监测
设想银行转账这个场景,并且有a,b,c三个账户,Id分别为1,2,3。转账这个流程是一个典型的原子性,b向a转账100,b的账户扣除100,a的账户增加100。要么都成功,要么都失败。
- 添加byte[] 类型的RowVer列,并且在OnModelCreating方法里注明改属性是.IsRowVersion()
public byte[] RowVer { get; set; }builder.Property(ba => ba.RowVer).IsRowVersion();
- 通过DbUpdateConcurrencyException捕获并发冲突
try
{await dbContext.SaveChangesAsync();return true;
}
catch (DbUpdateConcurrencyException ex)
{Console.WriteLine("并发冲突,正在重新加载数据...");// 处理并发冲突:重新加载当前数据,重新计算ex.Entries.Single().Reload();// 基于最新数据重新累加fromAccount.Balance -= amount;toAccount.Balance += amount;await dbContext.SaveChangesAsync();return true;
}
完整代码
public class Trade
{public async Task ExecuteAsync(){Console.Write("请输入您的账户ID:");long fromAccountId = Convert.ToInt64(Console.ReadLine());Console.Write("请输入您要转账的账户ID:");long toAccountId = Convert.ToInt64(Console.ReadLine());Console.Write("请输入转账金额:");decimal amount = Convert.ToDecimal(Console.ReadLine());Console.WriteLine("请确认是否继续:");Console.WriteLine("Y/N");string input = Console.ReadLine();if (input != "Y"){Console.WriteLine("取消转账");return;}Console.WriteLine($"正在转账 {amount} 元...${DateTime.Now}");bool isTransferring = true;// 启动后台计时器var timerTask = Task.Run(async () =>{while (isTransferring){Console.WriteLine($"处理中: {DateTime.Now}");await Task.Delay(1000);}});try{bool success = await TransferAsync(fromAccountId, toAccountId, amount,() => Thread.Sleep(5000));if (success){Console.WriteLine($"转账成功!{DateTime.Now}");}}finally{// 标记转账完成并等待计时器任务结束isTransferring = false;await timerTask; // 等待任务自然结束}Console.ReadLine();}public async Task<bool> TransferAsync(long fromAccountId, long toAccountId, decimal amount, Action sleep){using (ApplicationDbContext dbContext = new ApplicationDbContext()){var fromAccount = await dbContext.BankAccounts.FirstOrDefaultAsync(a => a.AccountID == fromAccountId);var toAccount = await dbContext.BankAccounts.FirstOrDefaultAsync(a => a.AccountID == toAccountId);if (sleep != null){sleep();}// 验证账户是否存在if (fromAccount == null || toAccount == null){throw new InvalidOperationException("账户不存在");}// 检查余额是否足够if (fromAccount.Balance < amount){throw new InvalidOperationException("余额不足");}// 执行转账fromAccount.Balance -= amount;toAccount.Balance += amount;try{await dbContext.SaveChangesAsync();return true;}catch (DbUpdateConcurrencyException ex){Console.WriteLine("并发冲突,正在重新加载数据...");// 处理并发冲突:重新加载当前数据,重新计算ex.Entries.Single().Reload();// 基于最新数据重新累加fromAccount.Balance -= amount;toAccount.Balance += amount;await dbContext.SaveChangesAsync();return true;}}}
}
接下来我们启动两个控制台程序,第一个程序通过3号账号向1号账号转账,第二个程序通过2号账号向1号账号转账。两个程序同时执行,我们观察执行结果。
成功捕捉到并发冲突。
此时数据库的RowVer便是数据库维护的令牌列
2.2 悲观锁
在EF Core中实现悲观锁主要是借助数据库的锁机制来实现,并显示结合事务。
同样是2.1.2的银行转账场景
- 首先我们通过EF Core显示开启事务
using (IDbContextTransaction transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable))
- 并且在转账逻辑完成的时候,提交事务
// 提交事务(释放锁)
await transaction.CommitAsync();
- 使用 SQL Server 原生语句查询获取账号并锁定账户,使用ROWLOCK, UPDLOCK锁定行数据的更新。
// 使用 SQL Server 原生语句查询并锁定账户(悲观锁)var fromAccount = await dbContext.BankAccounts.FromSql($"SELECT * FROM T_BankAccount WITH (ROWLOCK, UPDLOCK) WHERE AccountID = {fromAccountId}").SingleOrDefaultAsync();var toAccount = await dbContext.BankAccounts.FromSql($"SELECT * FROM T_BankAccount WITH (ROWLOCK, UPDLOCK) WHERE AccountID = {toAccountId}").SingleOrDefaultAsync();
4.观察执行转账,启动一个后台进程循环打印处理中,方便观察。通过isTransferring标识符来结束循环。
bool isTransferring = true;
// 启动后台计时器
var timerTask = Task.Run(async () =>
{while (isTransferring){Console.WriteLine($"处理中: {DateTime.Now}");await Task.Delay(1000);}
});
完整代码
public class Program
{static async Task Main(string[] args){Console.Write("请输入您的账户ID:");long fromAccountId = Convert.ToInt64(Console.ReadLine());Console.Write("请输入您要转账的账户ID:");long toAccountId = Convert.ToInt64(Console.ReadLine());Console.Write("请输入转账金额:");decimal amount = Convert.ToDecimal(Console.ReadLine());Console.WriteLine("请确认是否继续:");Console.WriteLine("Y/N");string input = Console.ReadLine();if (input != "Y"){Console.WriteLine("取消转账");return;}Console.WriteLine($"正在转账 {amount} 元...${DateTime.Now}");bool isTransferring = true;// 启动后台计时器var timerTask = Task.Run(async () =>{while (isTransferring){Console.WriteLine($"处理中: {DateTime.Now}");await Task.Delay(1000);}});try{bool success = await TransferMoneyAsync(fromAccountId, toAccountId, amount,() => Thread.Sleep(5000));if (success){Console.WriteLine($"转账成功!{DateTime.Now}");}}finally{// 标记转账完成并等待计时器任务结束isTransferring = false;await timerTask; // 等待任务自然结束}Console.ReadLine();}/// <summary>/// 执行转账/// </summary>/// <param name="fromAccountId"></param>/// <param name="toAccountId"></param>/// <param name="amount"></param>/// <param name="sleep"></param>/// <returns></returns>/// <exception cref="InvalidOperationException"></exception>public static async Task<bool> TransferMoneyAsync(long fromAccountId, long toAccountId, decimal amount, Action sleep){using (ApplicationDbContext dbContext = new ApplicationDbContext())using (IDbContextTransaction transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable)){try{// 使用 SQL Server 原生语句查询并锁定账户(悲观锁)var fromAccount = await dbContext.BankAccounts.FromSql($"SELECT * FROM T_BankAccount WITH (ROWLOCK, UPDLOCK) WHERE AccountID = {fromAccountId}").SingleOrDefaultAsync();var toAccount = await dbContext.BankAccounts.FromSql($"SELECT * FROM T_BankAccount WITH (ROWLOCK, UPDLOCK) WHERE AccountID = {toAccountId}").SingleOrDefaultAsync();if (sleep != null){sleep();}// 验证账户是否存在if (fromAccount == null || toAccount == null){throw new InvalidOperationException("账户不存在");}// 检查余额是否足够if (fromAccount.Balance < amount){throw new InvalidOperationException("余额不足");}// 执行转账fromAccount.Balance -= amount;toAccount.Balance += amount;// 更新数据库await dbContext.SaveChangesAsync();// 提交事务(释放锁)await transaction.CommitAsync();return true;}catch (Exception ex){// 发生异常时回滚事务await transaction.RollbackAsync();Console.WriteLine($"转账失败: {ex.Message}");return false;}}}
}
接下来我们启动两个控制台程序,第一个程序通过3号账号向1号账号转账,第二个程序通过2号账号向1号账号转账。两个程序同时执行,我们观察执行结果。
第二个程序一直等待第一个程序执行完成才开始执行转账。这是因为我们借助数据库的锁机制,把当前行的数据更新锁定了。换句话说,悲观锁确保同时只有一个使用者操作被锁定的资源,但会带来性能、死锁等方面的问题。
总结
本文通过代码示例演示如何解决并发导致的丢失更新问题,兼顾一致性与性能优化,详解 EF Core 中 乐观锁 和悲观锁 的实现方式。
相关文章:

【EF Core】 EF Core并发控制:乐观锁与悲观锁的应用
文章目录 前言一、并发的风险二、EF Core中的并发控制方式2.1 开放式并发(乐观锁)2.1.1 应用程序管理的属性并发令牌2.1.2 数据库生成的并发令牌 2.2 悲观锁 总结 前言 实际的生产环境中,我们经常能遇到数据库由多个应用程序同时使用。每个程…...
WaytoAGI东京大会开启AI全球化新对话:技术无国界,合作促创新
全球AI专家齐聚东京,一场关于技术无国界的对话正在进行。 2025年6月7日,一场备受瞩目的AI盛会——“WaytoAGI全球AI大会东京站”在日本东京樱美林大学新宿校区正式拉开帷幕。这场为期两天的会议(6月7日至8日)由国内最大的AI开源知…...

Harmony核心:动态方法修补与.NET游戏Mod开发
一、Harmony的核心定位与设计哲学 Harmony是一个运行时动态方法修补库,专为修改已编译的.NET/Mono应用程序而设计,尤其适用于游戏Mod开发。其核心创新在于: 非破坏性修改:保留原始方法完整性,避免直接替换或覆盖。多…...
AI系统应用开发工程师
以下是对AI系统应用开发与运维岗位的梳理整合,从企业、岗位、任务、能力等维度进行分类呈现,便于清晰对比两者的工作侧重: 一、代表性企业对比 分类企业名称应用开发方向中移系统集成有限公司、科大讯飞河北科技有限公司、华为技术服务有限…...
Qt Test功能及架构
Qt Test 是 Qt 框架中的单元测试模块,在 Qt 6.0 中提供了全面的测试功能。 一、主要功能 核心功能 1. 单元测试框架 提供完整的单元测试基础设施 支持测试用例、测试套件的组织和执行 包含断言宏和测试结果收集 2. 测试类型支持 单元测试:对单个函…...
图像处理、图像分析和图像理解的定义、联系与区别
1. 定义 图像处理(Image Processing) 图像处理是低层操作,主要针对像素级的图像数据进行加工,目的是改善图像质量或为后续分析做准备。 典型任务:去噪、增强(如对比度调整)、锐化、边缘检测、图…...

【Java开发日记】说一说 SpringBoot 中 CommandLineRunner
目录 1、CommandLineRunner SpringBoot中CommandLineRunner的作用 简单例子 多个类实现CommandLineRunner接口执行顺序的保证 通过实现Ordered接口实现控制执行顺序 通过Order注解实现控制执行顺序 Order 作用 2、ApplicationRunner 3、传递参数 4、源码跟踪 run()方…...

全面理解 Linux 内核性能问题:分类、实战与调优策略
在 Linux 系统(特别是嵌入式或服务器环境)中,性能问题往往错综复杂、表象多变。只有对常见性能问题进行系统归类、理解其症状与根源,才能有效定位和解决。本文将围绕八大类核心性能问题,结合实战示例,逐类分…...

算法-多条件排序
1、数对排序的使用 pair<ll,ll> a[31];//cmp为比较规则 ll cmp(pair<ll,ll>a,pair<ll,ll>b){if(a.first!b.first)return a.first>b.first;else return a.second<b.second; }//按照比较规则进行排序 sort(a1,a31,cmp); 2、具体例题 输入样例࿱…...
DelayQueue、ScheduledThreadPoolExecutor 和 PriorityBlockingQueue :怎么利用堆实现定时任务
DelayQueue DelayQueue 的最大亮点: 并不是简单全局锁的“单调队列”实现,而是用Leader-Follower 模式极大减少了线程唤醒的开销。插入与唤醒、等待与 leader 变更,都通过巧妙的锁和条件变量组合完成。 如果只关注“线程安全的优先队列全局…...
Kafka 消息模式实战:从简单队列到流处理(二)
四、Kafka 流处理实战 4.1 Kafka Streams 简介 Kafka Streams 是 Kafka 提供的流处理库,它为开发者提供了一套简洁而强大的 API,用于构建实时流处理应用程序。Kafka Streams 基于 Kafka 的高吞吐量、分布式和容错特性,能够处理大规模的实时…...
大数据(2) 大数据处理架构Hadoop
一、Hadoop简介 1.定义 Hadoop 是一个开源的分布式计算框架,由 Apache 基金会开发,用于处理海量数据,具备高可靠性、高扩展性和高容错性。它主要由两个核心模块组成: HDFS(Hadoop Distributed File System)…...
【Kotlin】注解反射扩展
文章目录 注解用法反射类引用 扩展扩展函数的作用域成员方法优先级总高于扩展函数 被滥用的扩展函数扩展属性静态扩展 标准库中的扩展函数 使用 T.also 函数交换两个变量sNullOrEmpty | isNullOrBlankwith函数repeat函数 调度方式对扩展函数的影响静态与动态调度扩展函数始终静…...

固定ip和非固定ip的区别是什么?如何固定ip地址
在互联网中,我们常会接触到固定IP和非固定IP的概念。它们究竟有何不同?如何固定IP地址?让我们一起来探究这个问题。 一、固定IP和非固定IP的区别是什么 固定IP(静态IP)和非固定IP(动态IP)是两种…...
升级centos 7.9内核到 5.4.x
前面是指南,后面是工作日志。 wget http://mirrors.coreix.net/elrepo-archive-archive/kernel/el7/x86_64/RPMS/kernel-lt-devel-5.4.225-1.el7.elrepo.x86_64.rpm wget http://mirrors.coreix.net/elrepo-archive-archive/kernel/el7/x86_64/RPMS/kernel-lt-5.4.2…...
Nginx 安全设置配置
1、增加header公共文件 文件地址:/etc/nginx/conf.d/security_headers.conf # XSS防护配置add_header X-XSS-Protection "1; modeblock" always; # 其他安全配置add_header X-Content-Type-Options "nosniff";add_header X-Frame-Options &qu…...
协程的常用阻塞函数
以下是一些常见的阻塞函数示例: 1. **Thread.sleep()** 阻塞当前线程一段时间。 kotlin Thread.sleep(1000) // 阻塞线程 1 秒 2. **InputStream.read()** 从输入流中读取数据时会阻塞,直到有数据可用或流结束。 kotlin val inputStream FileInputStre…...
探索NoSQL注入的奥秘:如何消除MongoDB查询中的前置与后置条件
随着互联网技术的飞速发展,数据库作为信息存储与管理的核心,其安全性问题日益凸显。近年来,NoSQL数据库因其灵活性和高性能逐渐成为许多企业的首选,其中MongoDB以其文档存储和JSON-like查询语言在开发社区中广受欢迎。然而&#x…...

使用矩阵乘法+线段树解决区间历史和问题的一种通用解法
文章目录 前言P8868 [NOIP2022] 比赛CF1824DP9990/2020 ICPC EcFinal G 前言 一般解决普通的区间历史和,只需要定义辅助 c h s − t ⋅ a chs-t\cdot a chs−t⋅a, h s hs hs是历史和, a a a是区间和, t t t是时间戳,…...
React Navive初识
文章目录 搭建开发环境安装 Node、homebrew、Watchman安装 Node安装 homebrew安装 watchman 安装 React Native 的命令行工具(react-native-cli)创建新项目编译并运行 React Native 应用在 ios 模拟器上运行 调试访问 App 内的开发菜单 搭建开发环境 在…...
scss(sass)中 的使用说明
在 SCSS(Sass)中,& 符号是一个父选择器引用,它代表当前嵌套规则的外层选择器。主要用途如下: 1. 连接伪类/伪元素 scss 复制 下载 .button {background: blue;&:hover { // 相当于 .button:hoverbackgrou…...

如何从浏览器中导出网站证书
以导出 GitHub 证书为例,点击 小锁 点击 导出 注意:这里需要根据你想要证书格式手动加上后缀名,我的是加 .crt 双击文件打开...

低功耗MQTT物联网架构Java实现揭秘
文章目录 一、引言二、相关技术概述2.1 物联网概述2.2 MQTT协议java三、基于MQTT的Iot物联网架构设计3.1 架构总体设计3.2 MQTT代理服务器选择3.3 物联网设备设计3.4 应用服务器设计四、基于MQTT的Iot物联网架构的Java实现4.1 开发环境搭建4.2 MQTT客户端实现4.3 应用服务器实现…...
总结HTML中的文本标签
总结HTML中的文本标签 文章目录 总结HTML中的文本标签引言一、标题标签(h1 - h6)语法示例使用建议 二、段落标签(p)语法示例使用建议 三、文本节点标签(span)语法示例使用建议 四、粗体标签(b&a…...
python版若依框架开发:前端开发规范
python版若依框架开发 从0起步,扬帆起航。 python版若依部署代码生成指南,迅速落地CURD!项目结构解析前端开发规范文章目录 python版若依框架开发新增 view新增 api新增组件新增样式引⼊依赖新增 view 在 @/views文件下 创建对应的文件夹,一般性一个路由对应⼀个文件, 该…...
AI推理服务的高可用架构设计
AI推理服务的高可用架构设计 在传统业务系统中,高可用架构主要关注服务冗余、数据库容灾、限流熔断等通用能力。而在AI系统中,尤其是大模型推理服务场景下,高可用架构面临更加复杂的挑战,如推理延迟敏感性、GPU资源稀缺性、模型版本切换频繁等问题。本节将专门探讨如何构建…...
GPU集群故障分析:大型AI训练中的硬件问题与影响
GPU集群故障分析:大型AI训练中的硬件问题与影响 核心问题 在大型AI计算集群(如使用上千块GPU卡训练大模型)中: GPU硬件会出哪些毛病?这些问题发生的频率、严重程度如何?最终对AI训练任务有什么影响&#…...

ideal2022.3.1版本编译项目报java: OutOfMemoryError: insufficient memory
最近换了新电脑,用新电脑拉项目配置后,启动时报错,错误描述 idea 启动Springboot项目在编译阶段报错:java: OutOfMemoryError: insufficient memory 2. 处理方案 修改VM参数,分配更多内存 ❌ 刚刚开始以为时JVM内存设置…...

centos7编译安装LNMP架构
一、LNMP概念 LNMP架构是一种常见的网站服务器架构,由Linux操作系统、Nginx Web服务器、MySQL数据库和PHP后端脚本语言组成。 1 用户请求:用户通过浏览器输入网址,请求发送到Nginx Web服务器。 2 Nginx处理:Nginx接收请求后&…...
接口限频算法:漏桶算法、令牌桶算法、滑动窗口算法
文章目录 限频三大算法对比与选型建议一、漏桶算法(Leaky Bucket Algorithm)1.核心原理2.实现3.为什么要限制漏桶容量4.优缺点分析 二、令牌桶算法(Token Bucket Algorithm)1.核心原理2.实现(1)单机实现&am…...