秒杀抢购场景下实战JVM级别锁与分布式锁
背景历史
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。
为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
锁机制的发展经历了从单机锁到分布式锁的过程。早期的系统大多运行在单机环境中,因此主要使用JVM级别的锁,如synchronized
和ReentrantLock
等。随着分布式系统的兴起,传统的JVM级别锁已经无法满足需求,于是分布式锁应运而生。分布式锁能够在多个节点之间协调对共享资源的访问,确保数据的一致性和系统的稳定性。
业务场景
秒杀抢购活动通常具有以下几个特点:
- 瞬时大流量:秒杀活动吸引大量用户参与,活动开始时会有海量的并发请求涌入系统。
- 热点数据:用户通常抢购的是同一商品,因此该商品的库存数据会成为热点数据,需要频繁读写。
- 数据一致性:秒杀活动需要确保数据的一致性,避免出现超卖或数据不一致的情况。
在秒杀抢购场景下,锁机制主要用于以下几个方面:
- 库存扣减:在用户下单时,需要确保库存扣减操作是原子性的,避免出现多个请求同时扣减库存导致超卖的情况。
- 订单生成:在用户下单成功后,需要生成订单并扣减库存,这个过程也需要确保原子性。
- 支付确认:在用户支付成功后,需要确认支付并释放库存,这个过程同样需要确保原子性。
底层原理
JVM级别锁
JVM级别锁是运行在单JVM进程中的锁机制,它主要通过Java对象头中的锁标记来实现。在Java中,每个对象都有一个对象头,对象头中包含了锁标记、哈希码等信息。根据锁的状态不同,锁标记的内容也会有所不同。
JVM级别锁主要包括以下几种:
- synchronized:
synchronized
是Java中最基本的锁机制,它可以用于修饰方法或代码块。当线程进入synchronized
修饰的方法或代码块时,会尝试获取对象的锁。如果锁已经被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。
synchronized
锁的实现原理可以归纳为以下几个步骤:
- 获取锁:当线程进入
synchronized
修饰的方法或代码块时,会检查对象头中的锁标记。如果锁标记为未加锁状态,则当前线程会尝试获取锁,并将锁标记设置为锁定状态。如果锁已经被其他线程持有,则当前线程会进入阻塞状态。 - 释放锁:当线程退出
synchronized
修饰的方法或代码块时,会释放锁,并将锁标记设置为未加锁状态。其他等待的线程会重新尝试获取锁。
synchronized
锁具有以下几个特点:
- 可重入:同一个线程可以多次获取同一个锁,而不会导致死锁。
- 不可中断:获取锁的线程在锁被释放之前无法被中断。
- 公平锁/非公平锁:
synchronized
锁默认是非公平锁,即线程获取锁的顺序不是按照请求的顺序来的。
- ReentrantLock:
ReentrantLock
是Java中另一种常用的锁机制,它提供了比synchronized
更灵活的锁控制。ReentrantLock
实现了Lock
接口,支持显式地获取和释放锁,以及设置锁的超时时间等。
ReentrantLock
的实现原理与synchronized
类似,也是通过改变对象头中的锁标记来实现锁的控制。不过,ReentrantLock
提供了更多的功能,如可重入性、公平锁/非公平锁、锁超时等。
- ReadWriteLock:
ReadWriteLock
是一种读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。ReadWriteLock
提供了更好的并发性能,适用于读多写少的场景。 - StampedLock:
StampedLock
是Java 8中引入的一种新的锁机制,它提供了一种乐观读锁的机制。StampedLock
允许线程在没有获取锁的情况下读取共享资源,但如果读取过程中资源被修改,则读取操作会失败。StampedLock
适用于读多写少的场景,且读操作对性能要求较高的场景。
分布式锁
分布式锁是运行在多个节点之间的锁机制,它能够在多个节点之间协调对共享资源的访问。分布式锁的实现通常依赖于一些外部的、可靠的存储或服务,如Redis、ZooKeeper、数据库等。
分布式锁主要包括以下几种:
- 基于数据库的分布式锁:基于数据库的分布式锁通过在数据库中创建一个锁表来实现。锁表中包含锁的名称和锁的状态等信息。当一个节点需要获取锁时,它会在锁表中插入一条记录。如果插入成功,则表示该节点获取到了锁;如果插入失败(因为其他节点已经插入了相同的记录),则表示该节点获取锁失败。当节点使用完锁后,会删除锁表中的记录以释放锁。
基于数据库的分布式锁具有实现简单的优点,但性能较低,且如果数据库出现故障,可能会影响到锁的功能。
- 基于Redis的分布式锁:基于Redis的分布式锁利用Redis的SETNX命令和EXPIRE命令来实现。SETNX命令用于在key不存在时设置值,这可以确保在同一时间只有一个客户端能够获得锁。EXPIRE命令用于为key设置过期时间,这可以避免死锁的情况。当一个客户端需要获取锁时,它会尝试使用SETNX命令来设置锁。如果命令返回OK,则表示客户端成功获取了锁;如果返回nil,则表示锁已被其他客户端持有。客户端在获取锁后,可以使用EXPIRE命令为锁设置过期时间。当客户端完成操作后,需要使用DEL命令来释放锁。
基于Redis的分布式锁具有性能高、实现简单的优点,但默认是不可重入的,且如果Redis服务器出现故障,可能会导致锁无法正常工作。
- 基于ZooKeeper的分布式锁:基于ZooKeeper的分布式锁利用ZooKeeper的临时节点来实现。当一个节点需要获取锁时,它会尝试在ZooKeeper中创建一个临时节点。如果创建成功,则表示该节点获取到了锁;如果创建失败(因为其他节点已经创建了相同的临时节点),则表示该节点获取锁失败。当节点使用完锁后,会删除临时节点以释放锁。如果节点崩溃,ZooKeeper会自动删除临时节点,从而避免了死锁的问题。
基于ZooKeeper的分布式锁具有高效且可靠的优点,但实现相对复杂一些。
- 基于Etcd的分布式锁:基于Etcd的分布式锁利用Etcd的键值存储系统来实现。当一个节点需要获取锁时,它会尝试在Etcd中创建一个带有TTL(Time To Live)的键值对。如果创建成功,则表示该节点获取到了锁;如果创建失败(因为其他节点已经创建了相同的键值对),则表示该节点获取锁失败。当节点使用完锁后,会删除键值对以释放锁。如果TTL过期而节点仍未释放锁,Etcd会自动删除键值对以释放锁。
基于Etcd的分布式锁具有实现简单、性能较高的优点,但同样需要处理Redis服务器故障等潜在问题。
Java代码实现
JVM级别锁实现
以下是一个使用synchronized
关键字实现秒杀抢购功能的Java代码示例:
public class SeckillService {
// 商品库存
private int stock = 10;
// 秒杀方法
public synchronized boolean seckill(String userId) {
if (stock <= 0) {
return false; // 库存不足}stock--; // 扣减库存System.out.println(userId + " 秒杀成功!剩余库存:" + stock);
return true;}
public static void main(String[] args) {
SeckillService service = new SeckillService();
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
String userId = "用户" + Thread.currentThread().getId();service.seckill(userId);}).start();}}
}
在这个示例中,SeckillService
类中的seckill
方法使用了synchronized
关键字进行修饰,以确保在同一时间只有一个线程能够执行该方法。当库存扣减成功后,会打印出秒杀成功的用户ID和剩余库存。
以下是一个使用ReentrantLock
实现秒杀抢购功能的Java代码示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SeckillService {
// 商品库存
private int stock = 10;
// ReentrantLock锁
private final Lock lock = new ReentrantLock();
// 秒杀方法
public boolean seckill(String userId) {lock.lock(); // 获取锁
try {
if (stock <= 0) {
return false; // 库存不足}stock--; // 扣减库存System.out.println(userId + " 秒杀成功!剩余库存:" + stock);
return true;} finally {lock.unlock(); // 释放锁}}
public static void main(String[] args) {
SeckillService service = new SeckillService();
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
String userId = "用户" + Thread.currentThread().getId();service.seckill(userId);}).start();}}
}
在这个示例中,SeckillService
类中使用了一个ReentrantLock
对象作为锁。在seckill
方法中,通过调用lock.lock()
方法来获取锁,并在finally
块中调用lock.unlock()
方法来释放锁。这样可以确保在出现异常时也能够正确释放锁。
分布式锁实现
以下是一个使用Redis实现分布式锁的Java代码示例:
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private final Jedis jedis;
private final String lockKey;
private final String uniqueValue;
private final int lockTimeout;
public RedisDistributedLock(Jedis jedis, String lockKey, int lockTimeout) {
this.jedis = jedis;
this.lockKey = lockKey;
this.uniqueValue = UUID.randomUUID().toString(); // 生成唯一值作为锁的持有者标识
this.lockTimeout = lockTimeout;}
// 尝试获取锁
public boolean tryLock() {
String result = jedis.set(lockKey, uniqueValue, "NX", "PX", lockTimeout);
return "OK".equals(result);}
// 释放锁
public void unlock() {
// 使用Lua脚本确保只有锁的持有者才能释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";jedis.eval(script, 1, lockKey, uniqueValue);}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
RedisDistributedLock lock = new RedisDistributedLock(jedis, "seckill_lock", 10000); // 锁超时时间为10秒
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
if (lock.tryLock()) {
try {
// 执行秒杀操作System.out.println(Thread.currentThread().getId() + " 秒杀成功!");} finally {lock.unlock(); // 确保释放锁}} else {System.out.println(Thread.currentThread().getId() + " 秒杀失败,锁已被占用");}}).start();}}
}
在这个示例中,RedisDistributedLock
类封装了Redis分布式锁的实现。构造函数中接收Jedis对象、锁键名、锁超时时间等参数。tryLock
方法尝试获取锁,如果获取成功则返回true,否则返回false。unlock
方法使用Lua脚本确保只有锁的持有者才能释放锁。在main
方法中,模拟了多个用户同时秒杀的场景,每个线程都会尝试获取锁并执行秒杀操作。
以下是一个使用ZooKeeper实现分布式锁的Java代码示例(需要引入ZooKeeper的客户端库):
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZookeeperDistributedLock {
private final ZooKeeper zooKeeper;
private final String lockPath;
private final CountDownLatch connectedSignal = new CountDownLatch(1);
public ZookeeperDistributedLock(String connectString, int sessionTimeout, String lockPath) throws IOException, InterruptedException {zooKeeper = new ZooKeeper(connectString, sessionTimeout, event -> {
if (event.getState() == Event.KeeperState.SyncConnected) {connectedSignal.countDown();}});connectedSignal.await();
this.lockPath = lockPath;}
// 尝试获取锁
public boolean tryLock() throws KeeperException, InterruptedException {
String createPath = zooKeeper.create(lockPath + "/lock_", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
String lockName = createPath.substring(lockPath.length() + 1);List<String> children = zooKeeper.getChildren(lockPath, false);children.sort(String::compareTo);
if (lockName.equals(children.get(0))) {
return true; // 获取锁成功} else {
String previousSequenceNode = lockPath + "/" + children.get(0);
Stat stat = zooKeeper.exists(previousSequenceNode, false);
if (stat != null) {zooKeeper.getData(previousSequenceNode, true, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted) {
try {tryLock();} catch (KeeperException | InterruptedException e) {e.printStackTrace();}}}});}
return false; // 获取锁失败}}
// 释放锁
public void unlock() throws KeeperException, InterruptedException {zooKeeper.delete(lockPath + "/lock_" + Thread.currentThread().getId(), -1);}
public static void main(String[] args) throws Exception {
ZookeeperDistributedLock lock = new ZookeeperDistributedLock("localhost:2181", 3000, "/locks");
// 模拟多个用户同时秒杀
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
if (lock.tryLock()) {
try {
// 执行秒杀操作System.out.println(Thread.currentThread().getId() + " 秒杀成功!");} finally {lock.unlock(); // 确保释放锁}} else {System.out.println(Thread.currentThread().getId() + " 秒杀失败,锁已被占用");}} catch (KeeperException | InterruptedException e) {e.printStackTrace();}}).start();}}
}
在这个示例中,ZookeeperDistributedLock
类封装了ZooKeeper分布式锁的实现。构造函数中接收ZooKeeper连接字符串、会话超时时间、锁路径等参数。tryLock
方法尝试获取锁,如果获取成功则返回true,否则返回false。在获取锁的过程中,会创建一个临时顺序节点,并根据节点的序号来判断是否获取到锁。如果当前节点是序号最小的节点,则表示获取锁成功;否则,会监听序号最小的节点的删除事件,以便在该节点被删除时重新尝试获取锁。unlock
方法用于释放锁,即删除当前节点。在main
方法中,模拟了多个用户同时秒杀的场景,每个线程都会尝试获取锁并执行秒杀操作。
总结
在秒杀抢购场景下,锁机制是确保数据一致性和系统稳定性的关键。JVM级别锁适用于单机环境,具有实现简单、性能较高等优点;而分布式锁则适用于分布式环境,能够在多个节点之间协调对共享资源的访问。在实际应用中,可以根据具体场景选择合适的锁机制来实现秒杀抢购功能。
通过本文的介绍,相信读者已经对JVM级别锁和分布式锁有了更深入的了解,并能够在实际项目中灵活运用这些技术来解决并发访问和数据一致性问题。
相关文章:
秒杀抢购场景下实战JVM级别锁与分布式锁
背景历史 在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极…...
【Pandas】pandas interval_range
Pandas2.2 General Top-level dealing with Interval data 方法描述interval_range([start, end, periods, freq, …])用于生成固定长度的区间序列 pandas.interval_range() pandas.interval_range() 是 Pandas 库中用于生成固定频率的 Interval 对象的函数。这些 Interval…...
有没有办法让爬虫更加高效,比如多线程处理?
要让Python爬虫更加高效,确实可以采用多线程处理。多线程可以显著提高爬虫的效率,因为它允许程序同时执行多个任务,从而减少等待时间。以下是一些提高爬虫效率的方法,特别是通过多线程技术: 1. 多线程爬虫 多线程爬虫…...

go-zero(十三)使用MapReduce并发
go zero 使用MapReduce并发 一、MapReduce 介绍 MapReduce 是一种用于并行计算的编程模型,特别适合在大规模数据处理场景中简化逻辑代码。 官方文档: https://go-zero.dev/docs/components/mr 1. MapReduce 的核心概念 在 MapReduce 中,主…...

【实操之 图像处理与百度api-python版本】
1 cgg带你建个工程 如图 不然你的pip baidu-aip 用不了 先对图片进行一点处理 $ 灰度处理 $ 滤波处理 参考 import cv2 import os def preprocess_images(input_folder, output_folder):# 确保输出文件夹存在if not os.path.exists(output_folder):os.makedirs(output_fol…...

java 导出word锁定且部分内容解锁可编辑
使用 Apache POI 创建带编辑限制的 Word 文档 在日常工作中,我们可能需要生成一些带有编辑限制的 Word 文档,例如某些段落只能被查看,而其他段落可以自由编辑。本文介绍如何使用 Apache POI 创建这样的文档,并通过代码实现相应的…...

SQL 在线格式化 - 加菲工具
SQL 在线格式化 打开网站 加菲工具 选择“SQL 在线格式化” 或者直接访问 https://www.orcc.online/tools/sql 输入sql,点击上方的格式化按钮即可 输入框得到格式化后的sql结果...
大数据法律法规——《关键信息基础设施安全保护条例》(山东省大数据职称考试)
大数据分析应用-初级 第一部分 基础知识 一、大数据法律法规、政策文件、相关标准 二、计算机基础知识 三、信息化基础知识 四、密码学 五、大数据安全 六、数据库系统 七、数据仓库. 第二部分 专业知识 一、大数据技术与应用 二、大数据分析模型 三、数据科学 大数据法律法规…...
【CVE-2024-5660】ARM CPU漏洞:硬件页面聚合(HPA)安全通告
安全之安全(security)博客目录导读 目录 一、概述 二、修改历史 三、什么是硬件页面聚合? 四、修复解决 一、概述 在一些基于arm的cpu中发现了一个问题,该问题可能允许修改的、不受信任的客户机操作系统...

数智读书笔记系列008 智人之上:从石器时代到AI时代的信息网络简史
书名:智人之上:从石器时代到AI时代的信息网络简史 作者:[以]尤瓦尔赫拉利 译者:林俊宏 出版时间:2024-09-01 ISBN:9787521768527 中信出版集团制作发行 作者信息 尤瓦尔・赫拉利 1976 年出生于以色列海法,是牛津大学历史学…...

将 Ubuntu 22.04 LTS 升级到 24.04 LTS
Ubuntu 24.04 LTS 将支持 Ubuntu 桌面、Ubuntu 服务器和 Ubuntu Core 5 年,直到 2029 年 4 月。 本文将介绍如何将当前 Ubuntu 22.04 系统升级到最新 Ubuntu 24.04 LTS版本。 备份个人数据 以防万一,把系统中的重要数据自己备份一下~ 安装配置SSH访问…...

【自动驾驶】Ubuntu20.04安装ROS1 Noetic
【自动驾驶】Ubuntu20.04安装ROS1 Noetic 方式一:官方教程方式二:鱼香ROS脚本安装ROS配置rosdep配置ROS环境 测试ROS1 Noetic是否安装成功 方式一:官方教程 https://wiki.ros.org/noetic/Installation/Ubuntu 方式二:鱼香ROS脚本 …...

(转,自阅,侵删)【LaTeX学习笔记】一文入门LaTeX(超详细)
【LaTeX学习笔记】一文入门LaTeX(超详细)-阿里云开发者社区LaTeX中主要分为导言区和正文区导言区通常用于定义文档的格式、语言等(全局设置)。常用的LaTex命令主要有\documentclass,\usepackage等。下面分别对几个常用…...
css的选择器有哪些?权重由大到小是怎么排序的?
CSS选择器有很多种,下面是常见的选择器类型,并按照其权重(即优先级)从高到低进行排序。 CSS选择器类型 通用选择器 (*) (通配符选择器) 选择所有元素,权重最低。 例如:* { color:…...

CTF知识集-PHP特性
title: CTF知识集-PHP特性 写在开头可能会用到的提示 call_user_func 调用的函数可以不区分大小写preg_match过滤存在长度溢出,长度超过100w检测失效。str_repeat(‘show’,250000); 生成100w个字符preg_match是无法处理数组的,例如:preg_match( n u m…...

比特币是否会取代美元(以及其他主权货币)
上图是 Olivier Blanchard 宏观经济学第八版的英文版内容。这里用中文解释。 1. 背景与现状: 比特币的规模与美元相比仍然很小: 截至 2018 年 12 月,比特币的总流通量为 1730 万枚,每枚价值 $3,900,总市值约 $670 亿…...
WPF+MVVM案例实战与特效(三十七)- 实现带有水印和圆角的自定义 TextBox 控件
文章目录 1、概述2、案例实现1、基本功能2、代码实现3、控件应用4、案例效果4、总结1、概述 在开发用户界面时,TextBox 是最常见的输入控件之一。为了提升用户体验,我们经常需要为 TextBox 添加一些额外的功能,例如显示提示文本(水印)和设置圆角边框。本文将详细介绍如何…...

深度学习训练参数之学习率介绍
学习率 1. 什么是学习率 学习率是训练神经网络的重要超参数之一,它代表在每一次迭代中梯度向损失函数最优解移动的步长,通常用 η \eta η 表示。它的大小决定网络学习速度的快慢。在网络训练过程中,模型通过样本数据给出预测值࿰…...

导游现场面试需要注意的问题
今天给大家带来一些导游现场面试需要注意的问题,大部分的城市导游考试已经考完了,但是还有一些城市的十二月份才考,有需要的朋友们赶紧来看,有备无患。 01、做好充足准备 认真准备做好每个景点的讲解介绍,不要抱有侥幸…...

Burp suite 3 (泷羽sec)
声明 学习视频来自B站UP主 泷羽sec,如涉及侵泷羽sec权马上删除文章。 笔记只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 这节课旨在扩大自己在网络安全方面的知识面,了解网络安全领域的见闻,了…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...

vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...