注解方式优雅的实现Redisson分布式锁

1.前言

随着微服务的快速推进,分布式架构也得到蓬勃的发展,那么如何保证多进程之间的并发则成为需要考虑的问题。因为服务是分布式部署模式,本地锁ReentrantlockSynchnorized就无法使用了,当然很多同学脱口而出的基于Redis的setnx锁由于上手简单,所以也被广泛使用,但是Redis的setnx锁存在无法保证原子性,所以Redisson目前备受推崇,今天我们一起来了解一下,并且用十分优雅的方式实现它。

当然实现分布式锁的方式有很多,像基于数据库表主键基于表字段版本号基于Redis的SETNX、REDLOCKREDISSON以及Zookeeper等方式来实现,本文对以上锁的实现以及优缺点不在讨论,有兴趣的可以移步至此:《分布式锁》

本文重点讲解一下Redisson分布式锁的实现

2.Redisson是如何基于Redis实现分布式锁的原理

先看一下最简单的实现方式:

    @Testvoid test1() {// 1、创建配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 2、根据 Config 创建出 RedissonClient 实例RedissonClient redissonClient = Redisson.create(config);//获取锁RLock lock = redissonClient.getLock("xxx-lock");try {// 2.加锁lock.lock();} finally {// 3.解锁lock.unlock();System.out.println("Finally,释放锁成功");}}

通过上面这段代码,我们看一下Redisson是如何基于Redis实现分布式锁的
下面的原理分析来自:《分布式锁-8.基于 REDISSON 做分布式锁》

2.1 加锁原理

通过上面的这段简单的代码,可以看出其加锁的方法主要依赖于其lock()方法,对于应的源码如下:

可以看到,调用getLock()方法后实际返回一个RedissonLock对象,在RedissonLock对象的lock()方法主要调用tryAcquire()方法

由于leaseTime == -1,于是走tryLockInnerAsync()方法,这个方法才是关键
首先,看一下evalWriteAsync方法的定义

<T, R> RFuture evalWriteAsync(String key, Codec codec, RedisCommand evalCommandType, String script, List keys, Object … params);

最后两个参数分别是keys和params

evalWriteAsync具体如何调用的呢?

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));

结合上面的参数声明,我们可以知道,这里

  • KEYS[1]就是getName()
  • ARGV[2]是getLockName(threadId)

假设前面获取锁时传的name是“abc”,假设调用的线程ID是Thread-1,假设成员变量UUID类型的id是6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c

那么KEYS[1]=abc,ARGV[2]=6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1

因此,这段代码想表达什么呢?

1、判断有没有一个叫“abc”的key

2、如果没有,则在其下设置一个字段为“6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1”,值为“1”的键值对 ,并设置它的过期时间

3、如果存在,则进一步判断“6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1”是否存在,若存在,则其值加1,并重新设置过期时间

4、返回“abc”的生存时间(毫秒)

这里用的数据结构是hash,hash的结构是: key 字段1 值1 字段2 值2 。。。

用在锁这个场景下,key就表示锁的名称,也可以理解为临界资源,字段就表示当前获得锁的线程

所有竞争这把锁的线程都要判断在这个key下有没有自己线程的字段,如果没有则不能获得锁,如果有,则相当于重入,字段值加1(次数)
算法原理如下图所示:

2.1 解锁原理

protected RFuture<Boolean> unlockInnerAsync(long threadId) {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +"end;" +"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; "+"end; " +"return nil;",Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));}

我们还是假设name=abc,假设线程ID是Thread-1

同理,我们可以知道

KEYS[1]是getName(),即KEYS[1]=abc

KEYS[2]是getChannelName(),即KEYS[2]=redisson_lock__channel:{abc}

ARGV[1]是LockPubSub.unlockMessage,即ARGV[1]=0

ARGV[2]是生存时间

ARGV[3]是getLockName(threadId),即ARGV[3]=6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1

因此,上面脚本的意思是:

1、判断是否存在一个叫“abc”的key

2、如果不存在,向Channel中广播一条消息,广播的内容是0,并返回1

3、如果存在,进一步判断字段6f0829ed-bfd3-4e6f-bba3-6f3d66cd176c:Thread-1是否存在

4、若字段不存在,返回空,若字段存在,则字段值减1

5、若减完以后,字段值仍大于0,则返回0

6、减完后,若字段值小于或等于0,则广播一条消息,广播内容是0,并返回1;

可以猜测,广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了

2.3 等待

上面的加锁,解锁均是 可以获取到锁资源的情况,那么当无法立即获取锁资源时,就需要等待

@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return;}//    订阅RFuture<RedissonLockEntry> future = subscribe(threadId);commandExecutor.syncSubscription(future);try {while (true) {ttl = tryAcquire(leaseTime, unit, threadId);// lock acquiredif (ttl == null) {break;}// waiting for messageif (ttl >= 0) {getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {getEntry(threadId).getLatch().acquire();}}} finally {unsubscribe(future, threadId);}
//        get(lockAsync(leaseTime, unit));
}protected static final LockPubSub PUBSUB = new LockPubSub();protected RFuture<RedissonLockEntry> subscribe(long threadId) {return PUBSUB.subscribe(getEntryName(), getChannelName(), commandExecutor.getConnectionManager().getSubscribeService());
}protected void unsubscribe(RFuture<RedissonLockEntry> future, long threadId) {PUBSUB.unsubscribe(future.getNow(), getEntryName(), getChannelName(), commandExecutor.getConnectionManager().getSubscribeService());
}

这里会订阅Channel,当资源可用时可以及时知道,并抢占,防止无效的轮询而浪费资源

当资源可用用的时候,循环去尝试获取锁,由于多个线程同时去竞争资源,所以这里用了信号量,对于同一个资源只允许一个线程获得锁,其它的线程阻塞

3.Redisson分布式锁常规使用

本章讲主要讲述加锁的常规使用,Redisson分布式锁是基于Redis的Rlock锁,实现了JavaJUC包下的Lock接口

3.1 添加maven依赖

        <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.8.2</version></dependency>

3.2 REDISSON的牛刀小试

还是原理中的那段代码,稍作修改

    @GetMapping("test1")public String test1() {// 1、创建配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 2、根据 Config 创建出 RedissonClient 实例RedissonClient redissonClient = Redisson.create(config);//获取锁RLock lock = redissonClient.getLock("xxx-lock");try {// 2.加锁lock.lock();System.out.println(new Date()+"获取锁成功");//业务代码Thread.sleep(1000 * 3);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 3.解锁lock.unlock();System.out.println("Finally,释放锁成功");}System.out.println("finish");return "finish";}

在这里插入图片描述

上面这段代码做的事情很简单:

getLock获取锁,lock.lock进行加锁,会出现的问题就是lock拿不到锁一直等待,会进入阻塞状态,显然这样是不好的。

1.TryLock

返回boolean类型,和Reentrantlock的tryLock是一个意思,尝试获取锁,获取到就返回true,获取失败就返回false,不会使获不到锁的线程一直处于等待状态,返回false可以继续执行下面的业务逻辑,当然Ression锁内部也涉及到watchDog看门狗机制,主要作用就是给快过期的锁进行续期,主要用途就是使拿到锁的有限时间让业务执行完,再进行锁释放。

为了避免频繁的去书写创建redis连接的代码,所以,我们将获取锁和释放锁的过程简单封装一下

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;@Component
public class LockUtil {static Map<String, RLock> lockMap = new ConcurrentHashMap<>();/*** 获取redisson客户端** @return*/public static final RedissonClient getClient() {// 1、创建配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 2、根据 Config 创建出 RedissonClient 实例RedissonClient redissonClient = Redisson.create(config);return redissonClient;}/*** 获取锁** @param lockName* @return*/public static boolean getLock(String lockName) {//获取锁RLock lock = getClient().getLock(lockName);try {if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {lockMap.put(lockName, lock);return true;}return false;} catch (InterruptedException e) {throw new RuntimeException(e);}}public static boolean getLock(String lockName, long waitTime, long leaseTime, TimeUnit timeUnit) {//获取锁RLock lock = getClient().getLock(lockName);try {if (lock.tryLock(waitTime, leaseTime, timeUnit)) {lockMap.put(lockName, lock);return true;}return false;} catch (InterruptedException e) {throw new RuntimeException(e);}}/*** 解锁** @param lockName*/public static void unLock(String lockName) {RLock lock = lockMap.get(lockName);if (Objects.nonNull(lock) && lock.isHeldByCurrentThread()) {lock.unlock();lockMap.remove(lockName);}}
}

使用方式如下:

    @GetMapping("test2")public void test2() {try {if (LockUtil.getLock("ninesun")) {//执行业务代码System.out.println("业务代码");}} catch (Exception e) {System.out.println("获取锁失败");e.printStackTrace();} finally {//释放锁LockUtil.unLock("ninesun");}}

为了使我们实现的方式更加优雅,下面我们通过注解来实现

2.自定义注解实现锁机制

通常我们都会将redisson实例注入到方法类里面,然后调用加锁方法进行加锁,如果其他业务方法也需要加锁执行,将会产生很多重复代码,由此采用AOP切面的方式,只需要通过注解的方式就能将方法进行加锁处理。

2.1 添加切面依赖
        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
2.2 自定义注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** @author ninesun* @ClassName RedissonDistributedLock* @description: TODO* @date 2023年11月27日* @version: 1.0*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedissonDistributedLock {String key() default "";int leaseTime() default 10;boolean autoRelease() default true;String errorDesc() default "系统正常处理,请稍后提交";int waitTime() default 1;TimeUnit timeUnit() default TimeUnit.SECONDS;
}
2.3 切面类实现
import com.example.demo.Utils.LockUtil;
import com.example.demo.annoation.RedissonDistributedLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;/*** @author ninesun* @ClassName RedisonDistributedLockHandler* @description: TODO* @date 2023年11月27日* @version: 1.0*/
@Aspect
@Component
public class RedisonDistributedLockHandler {private static final Logger log = LoggerFactory.getLogger(RedisonDistributedLockHandler.class);public RedisonDistributedLockHandler() {}@Around("@annotation(distributedLock)")public Object around(ProceedingJoinPoint joinPoint, RedissonDistributedLock distributedLock) throws Throwable {String lockName = this.getRedisKey(joinPoint, distributedLock);int leaseTime = distributedLock.leaseTime();String errorDesc = distributedLock.errorDesc();int waitTime = distributedLock.waitTime();TimeUnit timeUnit = distributedLock.timeUnit();Object var8;try {boolean lock = LockUtil.getLock(lockName, leaseTime, waitTime, timeUnit);if (!lock) {throw new RuntimeException(errorDesc);}var8 = joinPoint.proceed();} catch (Throwable var12) {log.error("执行业务方法异常", var12);throw var12;} finally {LockUtil.unLock(lockName);}return var8;}/*** 获取加锁的key** @param joinPoint* @param distributedLock* @return*/private String getRedisKey(ProceedingJoinPoint joinPoint, RedissonDistributedLock distributedLock) {String key = distributedLock.key();Object[] parameterValues = joinPoint.getArgs();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();String[] parameterNames = nameDiscoverer.getParameterNames(method);if (StringUtils.isEmpty(key)) {if (parameterNames != null && parameterNames.length > 0) {StringBuffer sb = new StringBuffer();int i = 0;for (int len = parameterNames.length; i < len; ++i) {sb.append(parameterNames[i]).append(" = ").append(parameterValues[i]);}key = sb.toString();} else {key = "redissionLock";}return key;} else {SpelExpressionParser parser = new SpelExpressionParser();Expression expression = parser.parseExpression(key);if (parameterNames != null && parameterNames.length != 0) {EvaluationContext evaluationContext = new StandardEvaluationContext();for (int i = 0; i < parameterNames.length; ++i) {evaluationContext.setVariable(parameterNames[i], parameterValues[i]);}try {Object expressionValue = expression.getValue(evaluationContext);return expressionValue != null && !"".equals(expressionValue.toString()) ? expressionValue.toString() : key;} catch (Exception var13) {return key;}} else {return key;}}}
}
2.4具体使用
    @GetMapping("test3")@RedissonDistributedLock(key = "'updateUserInfo:'+#id", errorDesc = "请勿重复提交")public void test3(@RequestParam(value = "id") String id) {//业务代码}

方法头加自定义注解

  • key参数代表需要加锁的key
  • errorDesc获取锁失败提示报错信息

在这里插入图片描述

在这里插入图片描述

上面的演示示例是单机模式,我们线上使用的可能是redis集群以及哨兵模式,这个只需控制我们redis的连接方式即可。

3.3 分布式集群

1.集群模式

这个需要我们redis中开启cluster nodes

       Config config = new Config();config.useClusterServers().setScanInterval(2000) // cluster state scan interval in milliseconds.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");RedissonClient redisson = Redisson.create(config);

2.哨兵模式

在使用哨兵模式时,需要创建SentinelServersConfig对象,并将其设置为Config对象的配置信息。代码创建SentinelServersConfig对象的方式如下:

SentinelServersConfig sentinelConfig = new SentinelServersConfig();
sentinelConfig.setMasterName("mymaster");
sentinelConfig.addSentinelAddress("redis://127.0.0.1:26379");
sentinelConfig.addSentinelAddress("redis://127.0.0.1:26380");
sentinelConfig.addSentinelAddress("redis://127.0.0.1:26381");
config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("redis://127.0.0.1:26379").addSentinelAddress("redis://127.0.0.1:26380").addSentinelAddress("redis://127.0.0.1:26381");

根据Redisson的官方文档,可以根据自己的需要来调整Redisson的各种参数,以达到最优的性能表现。以下是一些常用的配置参数及其说明。

  • connectTimeout:连接超时时间,单位:毫秒
  • timeout:读写超时时间,单位:毫秒
  • retryAttempts:连接失败重试次数,-1表示不限制重试次数
  • retryInterval:重试时间间隔,单位:毫秒
  • threads:响应请求线程数,最大为16

3.Redisson配置了集群不生效

3.4 Redisson配置序列化

为了提高Redisson的性能表现,Redisson在数据存储时使用了高效的序列化机制。在Redisson中,默认使用的是JDK序列化机制,但是考虑到JDK的序列化机制在序列化性能、序列化结果可读性、可靠性等方面存在一些问题,因此Redisson提供了多种序列化方式供用户选择。

常用的序列化方式有三种:JDK序列化、FastJSON序列化和Kryo序列化。其中,Kryo序列化是性能最高的一种序列化方式,但是需要注意的是,Kryo序列化与JDK序列化不兼容,因此在使用Kryo序列化时需要注意操作系统的类型及JDK的版本

如果要对Redisson的序列化机制进行定制,可以通过以下方式来实现。

// 基于Jackson序列化
SerializationConfig serialConfig = config.getCodec().getSerializationConfig();
serialConfig.setJacksonObjectMapper(new ObjectMapper());// 基于FastJSON序列化
SerializationConfig serialConfig = config.getCodec().getSerializationConfig();
serialConfig.setSerializer("com.alibaba.fastjson.JSON").setDecoder("com.alibaba.fastjson.JSON");// 基于Kryo序列化
SerializationConfig serialConfig = config.getCodec().getSerializationConfig();
Kryo kryo = new Kryo();
kryo.register(User.class);
kryo.register(Order.class);
kryo.register(Item.class);
kryo.register(ArrayList.class);
kryo.register(LinkedList.class);
kryo.register(RedisCommand.class);
UnicornKryoPool pool = new UnicornKryoPoolImpl(kryo);
serialConfig.setKryoPool(pool);

具体使用方式如下:

        //使用json序列化方式Codec codec = new JsonJacksonCodec();config.setCodec(codec);

至此单机模式下的基于Redission和注解实现的幂等控制就实现了,后面会将redis集群以及哨兵模式下的实现方式进行实现。


git地址:https://gitee.com/ninesuntec/distributed-locks.git

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

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

相关文章

使用canvas实现代码雨高级升阶版【附带源码和使用方法】

文章目录 前言基本绿色的彩色版本飘散雪花状后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端面试 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&…

C++ -- 每日选择题 -- Day2

第一题 1. 下面代码中sizeof(A)结果为&#xff08;&#xff09; #pragma pack(2) class A {int i;union U{char str[13];int i;}u;void func() {};typedef char* cp;enum{red,green,blue}color; }; A&#xff1a;20 B&#xff1a;21 C&#xff1a;22 D&#xff1a;24 答案及解析…

windows文件删除权限

一、普通文件 这里指的是所有可以被随意删除的文件。 二、可更改权限的文件 如果想要删除的文件无法被删除&#xff0c;那大概是权限不够&#xff0c;这时候&#xff1a;鼠标右键、属性、安全、编辑、选择相应的组或用户&#xff08;如果不知道哪个可以全选&#xff0c;反正…

MySQL C代码连接

环境&#xff1a;5.7.42-0ubuntu0.18.04.1 (Ubuntu) mysql接口介绍 初始化mysql指针 用来生成MySQL对象&#xff0c;返回值为MySQL*&#xff0c;MySQL*是MySQL对象的指针。 MySQL在mysql.h中是一个结构体 链接数据库 初始化完毕之后&#xff0c;必须先链接数据库&#xff…

App测试、H5测试及小程序测试

文章目录 前言一、App常见测试要点1.App功能测试1.1.App安装与卸载1.2.App升级测试1.3.App登陆测试1.4.离线测试1.5.触屏及操作测试1.6.App消息推送测试 2.AppUI界面测试3.App兼容性测试-适配/手机适配4.App中断测试5.App网络测试6.App安全测试7.App性能测试8.App测试与web测试…

数据结构(六):堆介绍及面试常考算法

一、堆介绍 1、定义 堆是一种图的树形结构&#xff0c;被用于实现“优先队列”&#xff08;priority queues&#xff09;。优先队列是一种数据结构&#xff0c;可以自由添加数据&#xff0c;但取出数据时要从最小值开始按顺序取出。在堆的树形结构中&#xff0c;各个顶点被称…

2023.11.27 使用anoconda搭建tensorflow环境

2023.11.27 使用anoconda搭建tensorflow环境 提供一个简便安装tensorflow的方法 1. 首先安装anoconda&#xff0c;安装过程略&#xff0c;注意安装的时候勾选安装anoconda prompt 2. 进入anoconda prompt 3. 建立python版本 conda create -n tensorflow1 python3.84. 激活t…

融资经理简历模板

这份简历内容&#xff0c;以综合柜员招聘需求为背景&#xff0c;我们制作了1份全面、专业且具有参考价值的简历案例&#xff0c;大家可以灵活借鉴。 融资经理简历在线编辑下载&#xff1a;百度幻主简历 求职意向 求职类型&#xff1a;全职 意向岗位&#xff1a;融资经理 …

VIR-SLAM代码分析3——VIR_VINS详解之estimator.cpp/.h

前言 续接上一篇&#xff0c;本本篇接着介绍VIR-SLAM中estimator.cpp/.h文件的函数&#xff0c;尤其是和UWB相关的相比于VINS改动过的函数&#xff0c;仍然以具体功能情况代码注释的形式进行介绍。 重点函数介绍 优化函数&#xff0c;代码是先优化&#xff0c;后边缘化。 …

java学校高校运动会报名信息管理系统springboot+jsp

课题研究方案&#xff1a; 结合用户的使用需求&#xff0c;本系统采用运用较为广泛的Java语言&#xff0c;springboot框架&#xff0c;HTML语言等关键技术&#xff0c;并在idea开发平台上设计与研发创业学院运动会管理系统。同时&#xff0c;使用MySQL数据库&#xff0c;设计实…

计算机网络(二)

&#xff08;八&#xff09;客户端软件设计的细节 A、解析协议号 客户端可能会需要通过协议名指定协议&#xff0c;但是Socket接口是用协议号指定的&#xff0c;这时候我们就需要使用getprotobyname()函数实现协议名到协议号的转换&#xff0c;该函数会返回一个指向protoent的…

Lighthouse(灯塔)—— Chrome浏览器强大的性能测试工具

本文浏览器版本参考如下&#xff1a; 一、认识Lighthouse Lighthouse 是 Google 开发的一款工具&#xff0c;用于分析网络应用和网页&#xff0c;收集现代性能指标并提供对开发人员最佳实践的意见。 为 Lighthouse 提供一个需要审查的网址&#xff0c;它将针对此页面运行一连…