1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)
目录
- 1.1 从减库存聊起
- 1.2 环境准备
- 1.3 简单实现减库存
- 1.4 演示超卖现象
- 1.5 jvm锁
- 1.6 三种情况导致Jvm本地锁失效
- 1、多例模式下,Jvm本地锁失效
- 2、Spring的事务导致Jvm本地锁失效
- 3、集群部署导致Jvm本地锁失效
- 1.7 mysql锁演示
- 1.7.1、一个sql
- 1.7.2、悲观锁
- 1.7.3、乐观锁
- 1.7.4、mysql锁总结
- 1.8 redis乐观锁
- 1.8.1 引入redis
- 1.8.2 redis乐观锁原理
- 1.8.3 redis乐观锁解决超卖问题
- 1.8.4 redis乐观锁的缺点
1.1 从减库存聊起
多线程并发安全问题最典型的代表就是超卖现象
库存在并发量较大情况下很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。
场景:商品S库存余量为5时,用户A和B同时来购买一个商品,此时查询库存数都为5,库存充足则开始减库存
用户A:update db_stock set stock = stock - 1 where id = 1
用户B:update db_stock set stock = stock - 1 where id = 1
并发情况下,更新后的结果可能是4,而实际的最终库存量应该是3才对 !!
1.2 环境准备
建表语句:
CREATE TABLE `db_stock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',`stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',`count` int(11) DEFAULT NULL COMMENT '库存量',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
表中数据如下:

创建分布式锁demo工程:
目录结构

pom.xml
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.46</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.4</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
application.yml配置文件:
server.port=10010
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.239.11:3306/atguigu_distributed_lock
spring.datasource.username=root
spring.datasource.password=houchen
DistributedLockApplication启动类:
@SpringBootApplication
@MapperScan("com.atguigu.distributed.lock.mapper")
public class DistributedLockApplication {public static void main(String[] args) {SpringApplication.run(DistributedLockApplication.class, args);}}
Stock实体类:
@Data
@TableName("db_stock")
public class Stock {@TableIdprivate Long id;private String productCode;private String stockCode;private Integer count;
}
StockMapper接口:
public interface StockMapper extends BaseMapper<Stock> {
}
1.3 简单实现减库存

@RestController
public class StockController {@Autowiredprivate StockService stockService;@GetMapping("stock/deduct")public String deduct(){this.stockService.deduct();return "hello stock deduct!!";}}@Service
public class StockService {@Autowiredprivate StockMapper stockMapper;public void deduct(){// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}
}
测试:

查看数据库:

在浏览器中一个一个访问时,每访问一次,库存量减1,没有任何问题。
1.4 演示超卖现象
使用jmeter压力测试工具,高并发下压测一下,添加线程组:并发100循环50次,即5000次请求。


启动测试,查看压力测试报告:

- Label 取样器别名,如果勾选Include group name ,则会添加线程组的名称作为前缀
- # Samples 取样器运行次数
- Average 请求(事务)的平均响应时间
- Median 中位数
- 90% Line 90%用户响应时间
- 95% Line 90%用户响应时间
- 99% Line 90%用户响应时间
- Min 最小响应时间
- Max 最大响应时间
- Error 错误率
- Throughput 吞吐率
- Received KB/sec 每秒收到的千字节
- Sent KB/sec 每秒收到的千字节
查看mysql数据库剩余库存数:还有4818

1.5 jvm锁
使用jvm锁(synchronized关键字或者ReetrantLock)试试:
/*** 使用jvm锁来解决超卖问题*/public synchronized void deduct() {// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}
重启tomcat服务,再次使用jmeter压力测试,效果如下:

可以看到,加锁之后,吞吐量减少了一倍多!
查看mysql数据库:

并没有发生超卖现象,完美解决。
原理
添加synchronized关键字之后,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象

1.6 三种情况导致Jvm本地锁失效
1、多例模式下,Jvm本地锁失效
原理:StockService有多个对象,不同的对象持有不同的锁,所以还是会有多个线程进入到 临界区 中
演示:
@Service
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockService {@Autowiredprivate StockMapper stockMapper;/*** 使用jvm锁来解决超卖问题*/public synchronized void deduct() {// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}
}
重启tomcat服务,再次使用jmeter压力测试,查看数据库,发现库存确实没有减到 0 ,发生超卖

2、Spring的事务导致Jvm本地锁失效
在加锁的地方加上 @Transactional 注解
@Transactionalpublic synchronized void deduct() {// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0) {stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}
重启tomcat服务,再次使用jmeter压力测试,查看数据库,发现库存确实没有减到 0 ,发生超卖

造成超卖的原因:
Spring事务默认的隔离级别是可重复读

解决办法
扩大锁的范围,将开启事务,提交事务也包括在锁的代码块中
@GetMapping("stock/deduct")public String deduct(){synchronized (this) {this.stockService.deduct();}return "hello stock deduct!!";}
3、集群部署导致Jvm本地锁失效
使用jvm锁在单工程单服务情况下确实没有问题,但是在集群情况下会怎样?
接下启动多个服务并使用nginx负载均衡
1)启动两个服务(端口号分别10010 10086),如下:

2)配置nginx 负载均衡
#user nobody;
worker_processes 1;#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;#pid logs/nginx.pid;events {worker_connections 1024;
}http {include mime.types;default_type application/octet-stream;sendfile on;upstream distributed {server localhost:10010;server localhost:10086;}server {listen 80;server_name localhost;location / {proxy_pass http://distributed;}}
}
3)在post中测试:http://localhost/stock/deduct (其中80是nginx的监听端口)

请求正常,说明nginx负载均衡起作用了
4) Jmeter压力测试
注意
- 先把数据库库存量还原到5000
- 重新配置访问路径 http://localhost:80/stock/deduct

两台机器时,吞吐量明显大于单个机器
查看数据库,库存不为0,表示多服务时,Jvm锁失效

5) 原因
每个服务都有自己的本地锁,所以无法锁住临界区,导致多线程的安全问题
1.7 mysql锁演示
除了使用jvm锁之外,还可以使用mysql自带的锁:悲观锁 或者 乐观锁
1.7.1、一个sql
update db_stock set count = count - 1 where product_code = '1001' and count >= #{count}
public void deduct() {this.stockMapper.updateStock("1001", 1);}public interface StockMapper extends BaseMapper<Stock> {@Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count}")int updateStock(@Param("productCode") String productCode, @Param("count") Integer count);
}
这种方式可以解决上述Jvm锁失效的三个问题
缺点:
1、确定好锁范围
当使用的是表锁时,会导致系统的吞吐量直线下降
什么情况下会使用行级锁
1)锁的查询或者更新条件必须是索引字段
2) 查询或者更新条件必须是具体值
2、一件商品多个仓库问题无法处理
3、无法记录仓库变化前后的状态
1.7.2、悲观锁
SELECT ... FOR UPDATE (悲观锁)
代码实现
改造StockService: 添加事务注解,去掉synchronized关键词
@Transactionalpublic void deduct() {Stock stocks = this.stockMapper.queryStockForUpdate("1001");if (stocks != null && stocks.getCount() > 0) {stocks.setCount(stocks.getCount() - 1);this.stockMapper.updateById(stocks);}}
在StockeMapper中定义selectStockForUpdate方法:
public interface StockMapper extends BaseMapper<Stock> {@Update("update db_stock set count = count - #{count} where product_code = #{productCode} and count >= #{count}")int updateStock(@Param("productCode") String productCode, @Param("count") Integer count);@Select("select * from db_stock where product_code = #{productCode} for update")Stock queryStockForUpdate(@Param("productCode") String productCode);
}
压力测试
注意:测试之前,需要把库存量改成5000。压测数据如下:比jvm锁性能高很多

mysql数据库存:

【注意】使用MySQL乐观锁时,也需要注意锁的粒度,尽量使用行级锁,否则系统吞吐量会降低
1.7.3、乐观锁
乐观锁是相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重试。
使用数据版本(Version)记录机制实现,这是乐观锁最常用的实现 方式。一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新。
给db_stock表添加version字段:

改造 StockService
/*** 使用MySQL乐观锁来解决库存超卖问题*/public void deduct() {// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){// 获取版本号Long version = stock.getVersion();stock.setCount(stock.getCount() - 1);// 每次更新 版本号 + 1stock.setVersion(stock.getVersion() + 1);// 更新之前先判断是否是之前查询的那个版本,如果不是重试if (this.stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version)) == 0) {deduct();}}}
重启后使用jmeter压力测试工具结果如下:


并发度比较低,说明乐观锁在并发量越大的情况下,性能越低(因为需要大量的重试);并发量越小,性能越高。
乐观锁存在的问题
- 高并发情况下,性能较低
- ABA问题
- 读写分离的情况下,可能会导致乐观锁不可靠
1.7.4、mysql锁总结
性能:一个sql > 悲观锁 > jvm锁 > 乐观锁
- 如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下。
优先选择:一个sql
-
如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁
-
如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试。
优先选择:mysql悲观锁
- 不推荐jvm本地锁。
1.8 redis乐观锁
1.8.1 引入redis
见我的博客 https://blog.csdn.net/hc1285653662/article/details/127564372 中的SpringDataRedis客户端
改造StockService
/*** 为了提高请求响应的速度,将库存放在redis中进行操作*/public void deduct() {// 先查询库存是否充足String stockStr = redisTemplate.opsForValue().get("stock:" + "1001");Long stock = Long.parseLong(stockStr);if (stock != null && stock > 0) {redisTemplate.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));}}
演示redis库存超卖
设置redis库存为 5000

jmeter启动测试,可以看到并发比无锁时候的mysql库存要高

查询redis库存,发现剩余库存不为0,所以发生超卖现象

1.8.2 redis乐观锁原理
使用watch命令监视某个key,如果在监视的过程中该key被某个客户端修改后,那么自身对于key的修改将会失败

1.8.3 redis乐观锁解决超卖问题
改造StockService
/*** 为了提高请求响应的速度,将库存放在redis中进行操作*/public void deduct() {// 监听 stock:1001redisTemplate.execute(new SessionCallback<Object>() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {operations.watch("stock:" + "1001");String stockStr = (String) operations.opsForValue().get("stock:" + "1001");Long stock = Long.parseLong(stockStr);if (stock != null && stock > 0) {operations.multi();operations.opsForValue().set("stock:" + "1001", String.valueOf(stock - 1));List exec = operations.exec();// 如果减库存失败,代表key别其他客户端修改了,则进行重试if (exec == null || exec.size() == 0) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}deduct();}return exec;}return null;}});}
查看测试结果:发现并发很低(可能因为我redis部署在阿里云上的docker里,网络开销导致并发很低),但是确实解决超卖问题


1.8.4 redis乐观锁的缺点
- 性能问题
相关文章:
1、传统锁回顾(Jvm本地锁,MySQL悲观锁、乐观锁)
目录 1.1 从减库存聊起1.2 环境准备1.3 简单实现减库存1.4 演示超卖现象1.5 jvm锁1.6 三种情况导致Jvm本地锁失效1、多例模式下,Jvm本地锁失效2、Spring的事务导致Jvm本地锁失效3、集群部署导致Jvm本地锁失效 1.7 mysql锁演示1.7.1、一个sql1.7.2、悲观锁1.7.3、乐观…...
【Java||牛客】DFS应用迷宫问题
step by step. 题目: 描述 定义一个二维数组 N*M ,如 5 5 数组下所示: int maze[5][5] { 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, }; 它表示一个迷宫,其中的1表示墙壁,0表示可…...
【vue】Vue中class样式的动态绑定
简介:Vue中class样式的绑定 1、字符串写法 使用场景:样式的类型不确定 写法: <div :class"xd_bg">测试账号</div> 手动触发样式改变 注意:字符串使用的是vue实例data中已有的属性 2、对象写法 使…...
机器学习深度学习——随机梯度下降算法(及其优化)
在我们没有办法得到解析解的时候,我们可以用过梯度下降来进行优化,这种方法几乎可以所有深度学习模型。 关于优化的东西,我自己曾经研究过智能排班算法和优化,所以关于如何找局部最小值,以及如何跳出局部最小值的一些基…...
【MTK平台】【wpa_supplicant】关于wpa_supplicant_8/src/p2p/p2p.c文件的介绍
本文主要介绍external/wpa_supplicant_8/src/p2p/p2p.c文件 先看下p2p_find 这个方法 P2P_find 主要用于 P2P(点对点)网络中查找其他对等方的功能。另外可以看到设置P2P模块的状态为 P2P_SEARCH int p2p_find(struct p2p_data *p2p, unsigned int tim…...
华为数通HCIP-流量过滤与转发路径控制
流量控制 分类:流量过滤、流量转发路径控制; 特点:1、作用于数据层面/转发层面; 2、不会影响路由表,针对转发流量生效; 实现步骤: 1、通过流量匹配工具匹配流量(ACL…...
SpringBoot中定时任务开启多线程避免多任务堵塞
场景 SpringBoot中定时任务与异步定时任务的实现: SpringBoot中定时任务与异步定时任务的实现_霸道流氓气质的博客-CSDN博客 使用SpringBoot原生方式实现定时任务,已经开启多线程支持,以上是方式之一。 除此之外还可通过如下方式。 为什…...
回归预测 | MATLAB实现SO-CNN-BiLSTM蛇群算法优化卷积双向长短期记忆神经网络多输入单输出回归预测
回归预测 | MATLAB实现SO-CNN-BiLSTM蛇群算法优化卷积双向长短期记忆神经网络多输入单输出回归预测 目录 回归预测 | MATLAB实现SO-CNN-BiLSTM蛇群算法优化卷积双向长短期记忆神经网络多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 Matlab实…...
入侵检测——IDS概述、签名技术
1. 什么是IDS? IDS(intrusion detection system)入侵检测系统,是一种对网络传输进行即时监视,在发现可疑传输时发出警报或者采取主动反应措施的网络安全设备。它会对系统的运行状态进行监视,发现各种攻击企…...
golang 标准库json Marshal 序列化与反序列化
标准库代码 func Marshal(v any) ([]byte, error) {e : newEncodeState()defer encodeStatePool.Put(e)err : e.marshal(v, encOpts{escapeHTML: true})if err ! nil {return nil, err}buf : append([]byte(nil), e.Bytes()...)return buf, nil }func Unmarshal(data []byte, …...
【【51单片机AD/DA的分析】】
51单片机AD/DA的分析 看似单片机实验,其实是要学好数电 模数转换 与 数模转换 运算放大器 DA的转换就是利用运算放大器实现的 输出电压v0-(D7~D0)/256 x (VrefxRfb)/R D7~D0 就是我们控制的按键看输入多少 然后再划分256份 Vref是我们设置的一个基准电压 PWM 这种…...
在docker中安装使用达梦数据库
关于在docker中安装达梦数据库,达梦官方网站其实是有提供安装使用方法的,但可能还是有朋友不会,这里将在原文基础上简单扩充下。 注意:docker容器中,数据库安装后没有创建服务的脚本,只有bin、bin2、conf、…...
Leetcode-每日一题【剑指 Offer II 010. 和为 k 的子数组】
题目 给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。 示例 1: 输入:nums [1,1,1], k 2输出: 2解释: 此题 [1,1] 与 [1,1] 为两种不同的情况 示例 2: 输入:nums [1,2,3], k 3输出: 2 提示: 1 < nums.leng…...
【JavaScript】使用Promise来处理异步调用,方法传入参数为接口,并回调接口的方法
例如我们在下面这个方法传入一个接口,并将方法的执行过程用传入的接口进行回调 connect() {wx.connectSocket({url: this.url,success: () > {console.log(WebSocket 连接创建成功);},fail: (err) > {console.error(WebSocket 连接创建失败, err);}});wx.onS…...
grid map学习笔记1之Ubuntu18.04+ROS-melodic编译安装grid_map栅格地图及示例运行
文章目录 0 引言1 安装依赖和编译1.1 安装依赖1.2 下载编译 2 运行示例2.1 simple_demo2.2 tutorial_demo2.3 iterators_demo2.4 image_to_gridmap_demo2.5 grid_map_to_image_demo2.6 opencv_demo2.7 resolution_change_demo2.8 filters_demo2.9 interpolation_demo 0 引言 苏…...
postgres wal2json插件jsonb字段数据丢失问题解决
使用pgwal2jsondebezium进行数据同步时,发现偶尔会有jsonb字段数据丢失的问题 进行测试时发现: 1、发生数据丢失的jsonb字段长度都比较大(超过toast阈值,使用toast表存储) 2、针对发生jsonb字段丢失的数据,jsonb字段本身未发生修…...
华为eNSP:路由引入
一、拓扑图 二、路由器的配置 1、配置路由器的IP AR1: [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 1.1.1.1 24 [Huawei-GigabitEthernet0/0/0]qu AR2: [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 1.1.1.2 24 [Huaw…...
Retrospectives on the Embodied AI Workshop(嵌入式人工智能研讨会回顾) 论文阅读
论文信息 题目:Retrospectives on the Embodied AI Workshop 作者:Matt Deitke, Dhruv Batra, Yonatan Bisk 来源:arXiv 论文地址:https://arxiv.org/pdf/2210.06849 Abstract 我们的分析重点关注 CVPR Embodied AI Workshop 上…...
「JVM」Full GC和Minor GC、Major GC
Full GC和Minor GC、Major GC 一、Full GC1、什么是Full GC?2、什么情况下会触发full gc? 二、Minor GC1、什么是Minor GC?2、什么情况下会触发Minor GC? 三、Major GC1、什么是Major GC?2、什么情况下会触发Major GC?…...
Asp.Net MVC 使用Log4Net
Asp.Net MVC 使用Log4Net 在 ASP.NET MVC 中使用 Log4net 需要进行一些配置和代码集成。下面是在 ASP.NET MVC 中使用 Log4net 的步骤: 1. 安装 Log4net NuGet 包 打开 NuGet 包管理器控制台,并运行以下命令来安装 Log4net: Install-Pack…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
