分布式锁--redission 最佳实践!

news2024/11/25 22:38:17

我们知道如果我们的项目服务不只是一个实例的时候,单体锁就不再适用,而我们自己去用redis实现分布式锁的话,会有比如锁误删、超时释放、锁的重入、失败重试、Redis主从一致性等等一系列的问题需要自己解决。

当然,上述问题并非无法解决,只不过会比较麻烦。例如:

  • 原子性问题:可以利用Redis的LUA脚本来编写锁操作,确保原子性
  • 超时问题:利用WatchDog(看门狗)机制,获取锁成功时开启一个定时任务,在锁到期前自动续期,避免超时释放。而当服务宕机后,WatchDog跟着停止运行,不会导致死锁。
  • 锁重入问题:可以模拟Synchronized原理,放弃setnx,而是利用Redis的Hash结构来记录锁的持有者以及重入次数,获取锁时重入次数+1,释放锁是重入次数-1,次数为0则锁删除
  • 主从一致性问题:可以利用Redis官网推荐的RedLock机制来解决

这些解决方案实现起来比较复杂,因此我们通常会使用一些开源框架来实现分布式锁,而不是自己来编码实现。目前对这些解决方案实现的比较完善的一个第三方组件:Redisson

因此,我们只要会使用Redisson,即可解决上述问题,无需自己动手编码了。

下面就将介绍Redission在项目实战中的最佳实践。

快速入门

首先引入依赖:

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>

然后是配置:

 @Configuration
 public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 
        config.useSingleServer()
            .setAddress("redis://localhost:6379")
            .setPassowrd("root");
        // 创建客户端
        return Redisson.create(config);
    }
 }

最后是基本用法:

@Autowired
 private RedissonClient redissonClient;

 @Test
 void testRedisson() throws InterruptedException {
    // 1.获取锁对象,指定锁名称
    RLock lock = redissonClient.getLock("anyLock");
    try {
        // 2.尝试获取锁,参数:waitTime、leaseTime、时间单位
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        if (!isLock) {
            // 获取锁失败处理 ..
        } else {
            // 获取锁成功处理
        }
    } finally {
        // 4.释放锁
        lock.unlock();
    }
 }

利用Redisson获取锁时可以传3个参数:

  • waitTime:获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
  • leaseTime:锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效。
  • TimeUnit:时间单位

项目集成

common模块已经完成了Redission的基础配置:

@Slf4j
@ConditionalOnClass({RedissonClient.class, Redisson.class})
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfig {
    private static final String REDIS_PROTOCOL_PREFIX = "redis://";
    private static final String REDISS_PROTOCOL_PREFIX = "rediss://";

    @Bean
    @ConditionalOnMissingBean
    public LockAspect lockAspect(RedissonClient redissonClient){
        return new LockAspect(redissonClient);
    }

    @Bean
    @ConditionalOnMissingBean
    public RedissonClient redissonClient(RedisProperties properties){
        log.debug("尝试初始化RedissonClient");
        // 1.读取Redis配置
        RedisProperties.Cluster cluster = properties.getCluster();
        RedisProperties.Sentinel sentinel = properties.getSentinel();
        String password = properties.getPassword();
        int timeout = 3000;
        Duration d = properties.getTimeout();
        if(d != null){
            timeout = Long.valueOf(d.toMillis()).intValue();
        }
        // 2.设置Redisson配置
        Config config = new Config();
        if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){
            // 集群模式
            config.useClusterServers()
                    .addNodeAddress(convert(cluster.getNodes()))
                    .setConnectTimeout(timeout)
                    .setPassword(password);
        }else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){
            // 哨兵模式
            config.useSentinelServers()
                    .setMasterName(sentinel.getMaster())
                    .addSentinelAddress(convert(sentinel.getNodes()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }else{
            // 单机模式
            config.useSingleServer()
                    .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort()))
                    .setConnectTimeout(timeout)
                    .setDatabase(0)
                    .setPassword(password);
        }
        // 3.创建Redisson客户端
        return Redisson.create(config);
    }

几个关键点:

  • 这个配置上添加了条件注解@ConditionalOnClass({RedissonClient.class, Redisson.class}) 也就是说,只要引用了common模块,并且引用了Redisson依赖,这套配置就会生效。不引入Redisson依赖,配置自然不会生效,从而实现按需引入。
  • RedissonClient的配置无需自定义Redis地址,而是直接基于SpringBoot中的Redis配置即可。而且不管是Redis单机、Redis集群、Redis哨兵模式都可以支持

所以,在微服务中应用的步骤:

  • 引入common、Redisson依赖
  • 注入RedissonClient,使用分布式锁

应用到项目中:

在这里插入图片描述

通用分布式锁组件

Redisson的分布式锁使用并不复杂,基本步骤包括:

  • 1)创建锁对象
  • 2)尝试获取锁
  • 3)处理业务
  • 4)释放锁

但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多:

在这里插入图片描述

可以发现,非业务代码格式固定,每次获取锁总是在重复编码。我们可不可以对这部分代码进行抽取和简化呢?

实现思路分析

要优化这部分代码,需要通过整个流程来分析:

在这里插入图片描述

可以发现,只有红框部分是业务功能,业务前、后都是固定的锁操作。既然如此,我们完全可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强

但是,我们该如何标记这些切入点呢?

不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?

最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTime、releaseTime等等,都可以基于注解来传参。

因此,注解的核心作用是两个:

  • 标记切入点
  • 传递锁参数

综上,我们计划利用注解来标记切入点,传递锁参数。同时利用AOP环绕增强来实现加锁、释放锁等操作。

定义注解

注解本身起到标记作用,同时还要带上锁参数:

  • 锁名称
  • 锁等待时间
  • 锁超时时间
  • 时间单位

代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
    String name();

    long waitTime() default 1;

    long leaseTime() default -1;

    TimeUnit unit() default TimeUnit.SECONDS;
}

定义切面

接下来,我们定义一个环绕增强的切面,实现加锁、释放锁:

代码实现如下:

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered{

    private final RedissonClient redissonClient;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
        // 1.创建锁对象
        RLock lock = redissonClient.getLock(myLock.name());
        // 2.尝试获取锁
        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        // 3.判断是否成功
        if(!isLock) {
            // 3.1.失败,快速结束
            throw new BizIllegalException("请求太频繁");
        }
        try {
            // 3.2.成功,执行业务
            return pjp.proceed();
        } finally {
            // 4.释放锁
            lock.unlock();
        }
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

注意,Spring中的AOP切面有很多,会按照Order排序,按照Order值从小到大依次执行。Spring事务AOP的order值是Integer.MAX_VALUE,优先级最低。

我们的分布式锁一定要先于事务执行,因此,我们的切面一定要实现Ordered接口,指定order值小于Integer.MAX_VALUE即可。

使用锁

定义好了锁注解和切面,接下来就可以改造业务了:

在这里插入图片描述

可以看到,业务中无需手动编写加锁、释放锁的逻辑了,没有任何业务侵入,使用起来也非常优雅。

不过呢,现在还存在几个问题:

  • Redisson中锁的种类有很多,目前的代码中把锁的类型写死了
  • Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置
  • 锁的名称目前是写死的,并不能根据方法参数动态变化

所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。

工厂模式切换锁类型

Redisson中锁的类型有多种,比如:

可重入锁(Reentrant Lock)

  • 允许同一个线程多次获取同一个锁,计数器记录获取次数,释放时需要调用相同次数。

公平锁(Fair Lock)

  • 按照请求锁的顺序来获取锁,避免饥饿情况。先请求锁的线程会先获得锁。

非公平锁(Unfair Lock)

  • 允许线程抢占锁,可能会导致某些线程被长时间阻塞。性能较公平锁更好,但可能导致饥饿。

读写锁(Read-Write Lock)

  • 允许多个线程同时读取资源,但在写入时会独占锁。适合读多写少的场景。

信号量(Semaphore)

  • 允许多个线程获取一定数量的许可,适合限制资源的并发访问。

限流器(Rate Limiter)

  • 控制操作的频率,限制单位时间内的请求数量。

多节点锁(Multi-node Lock)

  • 支持在分布式环境中使用,能够在多个 Redis 节点上进行锁的管理。

因此,我们不能在切面中把锁的类型写死,而是交给用户自己选择锁类型。

那么问题来了,如何让用户选择锁类型呢?

锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock注解的参数,交给用户去选择自己要用的类型。

而在切面中,我们则需要根据用户选择的锁类型,创建对应的锁对象即可。但是这个逻辑不能通过if-else来实现,太low了。

这里我们的需求是根据用户选择的锁类型,创建不同的锁对象。有一种设计模式刚好可以解决这个问题:简单工厂模式

锁类型枚举

我们首先定义一个锁类型枚举:

具体代码:

public enum MyLockType {
    RE_ENTRANT_LOCK, // 可重入锁
    FAIR_LOCK, // 公平锁
    READ_LOCK, // 读锁
    WRITE_LOCK, // 写锁
    ;
}

然后在自定义注解中添加锁类型这个参数:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
    String name();

    long waitTime() default 1;

    long leaseTime() default -1;

    TimeUnit unit() default TimeUnit.SECONDS;

    MyLockType lockType() default  MyLockType.RE_ENTRANT_LOCK;//默认可重入

}
锁对象工厂

然后定义一个锁工厂,用于根据锁类型创建锁对象:

具体代码:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;



@Component
public class MyLockFactory {

    private final Map<MyLockType, Function<String, RLock>> lockHandlers;

    public MyLockFactory(RedissonClient redissonClient) {
        this.lockHandlers = new EnumMap<>(MyLockType.class);
        this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);
        this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);
        this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());
        this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
    }

    public RLock getLock(MyLockType lockType, String name){
        return lockHandlers.get(lockType).apply(name);
    }
}
  • MyLockFactory内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。
  • MyLockFactory内部的Map采用了EnumMap。只有当Key是枚举类型时可以使用EnumMap,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。
改造切面代码

我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:

在这里插入图片描述

此时,在业务中,就能通过注解来指定自己要用的锁类型了:

在这里插入图片描述

锁失败策略

多线程争抢锁,大部分线程会获取锁失败,而失败后的处理方案和策略是多种多样的。目前,我们获取锁失败后就是直接抛出异常,没有其它策略,这与实际需求不一定相符。

策略分析

接下来,我们就分析一下锁失败的处理策略有哪些。

大的方面来说,获取锁失败要从两方面来考虑:

  • 获取锁失败是否要重试?有三种策略:
    • 不重试,对应API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
    • 有限次数重试:对应API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重试一定waitTime时间后结束
    • 无限重试:对应API lock.lock(10, SECONDS) , lock就是无限重试
  • 重试失败后怎么处理?有两种策略:
    • 直接结束
    • 抛出异常

对应的API和策略名如下:

在这里插入图片描述

重试策略 + 失败策略组合,总共以下几种情况:

在这里插入图片描述

那么该如何用代码来表示这些失败策略,并让用户自由选择呢?

相信大家应该能想到一种设计模式:策略模式。同时,我们还需要定义一个失败策略的**枚举。**在MyLock注解中定义这个枚举类型的参数,供用户选择。

一般的策略模式大概是这样:

  • 定义策略接口
  • 定义不同策略实现类
  • 提供策略工厂,便于根据策略枚举获取不同策略实现

而在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式。

综上,我们可以定义一个基于枚举的策略模式,简化开发。

策略实现

我们定义一个失败策略枚举:

然后直接将失败策略定义到枚举中:

public enum MyLockStrategy {
    SKIP_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(0, prop.leaseTime(), prop.unit());
        }
    },
    FAIL_FAST(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    },
    KEEP_TRYING(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            lock.lock( prop.leaseTime(), prop.unit());
            return true;
        }
    },
    SKIP_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
        }
    },
    FAIL_AFTER_RETRY_TIMEOUT(){
        @Override
        public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
            boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
            if (!isLock) {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    },
    ;

    public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}

然后,在MyLock注解中添加枚举参数:在这里插入图片描述

最后,修改切面代码,基于用户选择的策略来处理:

在这里插入图片描述

这个时候,我们就可以在使用锁的时候自由选择锁类型、锁策略了:

基于SPEL的动态锁名

现在还剩下最后一个问题,就是锁名称的问题。

在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?

Spring中提供了一种表达式语法,称为SPEL表达式,可以执行java代码,获取任意参数。

我们可以让用户指定锁名称参数时不要写死,而是基于SPEL表达式。在创建锁对象时,解析SPEL表达式,动态获取锁名称。

SPEL表达式

SpEL(Spring Expression Language)是一种强大的表达式语言,允许在 Spring 中使用字符串表达式来操作对象图、调用方法、访问属性等。SpEL 的主要用途包括但不限于:动态属性访问、条件表达式、方法调用等。

基本语法

  1. 属性访问

    expression = "user.name"  // 访问 user 对象的 name 属性
    
  2. 方法调用

    expression = "user.getName()"  // 调用 user 对象的 getName() 方法
    
  3. 集合操作

    expression = "users.?[age > 18]"  // 过滤出年龄大于18的用户
    
  4. 条件表达式

    expression = "age > 18 ? 'Adult' : 'Minor'"  // 三元运算符
    
  5. 运算符

    • 算术运算符:+, -, *, /, %
    • 比较运算符:==, !=, <, >, <=, >=
    • 逻辑运算符:and, or, not

具体官网链接:8. Spring 表达式语言 (SpEL) (itmyhome.com)

首先,在使用锁注解时,锁名称可以利用SPEL表达式,例如我们指定锁名称中要包含参数中的用户id,则可以这样写:

在这里插入图片描述

而如果是通过UserContext.getUser()获取,则可以利用下面的语法:

在这里插入图片描述

这里T(类名).方法名()就是调用静态方法。

解析SPEL

在切面中,我们需要基于注解中的锁名称做动态解析,而不是直接使用名称:

在这里插入图片描述

其中获取锁名称用的是getLockName()这个方法:

/**
 * SPEL的正则规则
 */
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
 * 方法参数解析器
 */
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

/**
 * 解析锁名称
 * @param name 原始锁名称
 * @param pjp 切入点
 * @return 解析后的锁名称
 */
private String getLockName(String name, ProceedingJoinPoint pjp) {
    // 1.判断是否存在spel表达式
    if (StringUtils.isBlank(name) || !name.contains("#")) {
        // 不存在,直接返回
        return name;
    }
    // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
    EvaluationContext context = new MethodBasedEvaluationContext(
            TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
    // 3.构建SPEL解析器
    ExpressionParser parser = new SpelExpressionParser();
    // 4.循环处理,因为表达式中可以包含多个表达式
    Matcher matcher = pattern.matcher(name);
    while (matcher.find()) {
        // 4.1.获取表达式
        String tmp = matcher.group();
        String group = matcher.group(1);
        // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
        Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
        // 4.3.解析出表达式对应的值
        Object value = expression.getValue(context);
        // 4.4.用值替换锁名称中的SPEL表达式
        name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
    }
    return name;
}

private Method resolveMethod(ProceedingJoinPoint pjp) {
    // 1.获取方法签名
    MethodSignature signature = (MethodSignature)pjp.getSignature();
    // 2.获取字节码
    Class<?> clazz = pjp.getTarget().getClass();
    // 3.方法名称
    String name = signature.getName();
    // 4.方法参数列表
    Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
    return tryGetDeclaredMethod(clazz, name, parameterTypes);
}

private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
    try {
        // 5.反射获取方法
        return clazz.getDeclaredMethod(name, parameterTypes);
    } catch (NoSuchMethodException e) {
        Class<?> superClass = clazz.getSuperclass();
        if (superClass != null) {
            // 尝试从父类寻找
            return tryGetDeclaredMethod(superClass, name, parameterTypes);
        }
    }
    return null;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3dsMax合并FBX的时候相同的节点会被合并(重命名解决),3Ds MAX创建空物体(虚拟对象或者点)

3dsMax合并FBX的时候相同的节点会被合并 3dsamax的文档&#xff0c;但是并没有说FBX的合并如何处理 https://help.autodesk.com/view/3DSMAX/2024/CHS/?guidGUID-98146EB8-436F-4954-8682-C57D4E53262A模型节点信息&#xff0c;yase&#xff0c;Points&#xff0c;Mesh 都是点…

【优选算法】(第二十一篇)

目录 外观数列(medium) 题目解析 讲解算法原理 编写代码 数⻘蛙&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 外观数列(medium) 题目解析 1.题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 2.题目描述 给定⼀个正整数n&#xff0…

openpnp - 坐标文件中的元件0角度如果和编带规定的角度不一样,需要调整贴片任务中的元件旋转角度

文章目录 openpnp - 坐标文件中的元件0角度如果和编带规定的角度不一样&#xff0c;需要调整贴片任务中的元件旋转角度笔记查看自己图纸中的封装的0角度方法贴片任务的角度值范围编带规定的0角度根据编带规定的元件0角度来调整贴片的元件旋转角度如果是托盘飞达备注备注END ope…

电脑失声,一招搞定

早已习惯了Edge浏览器的“大声朗读”功能&#xff0c;今天值班&#xff0c;值班室用的两台电脑只配有耳机&#xff0c;没有音箱&#xff0c;顿时感觉不适。 先找了一个带功放的老音箱&#xff0c;发现少了电箱到功放的音频线。 一顿搜索&#xff0c;在找到音频线的同时&#…

2024年计算机视觉与艺术研讨会(CVA 2024)

目录 基本信息 大会简介 征稿主题 会议议程 参会方式 基本信息 大会官网&#xff1a;www.icadi.net&#xff08;点击了解参会投稿等信息&#xff09; 大会时间&#xff1a;2024年11月29-12月1日 大会地点&#xff1a;中国-天津 大会简介 2024年计算机视觉与艺术国际学术…

基于SpringBoot+Vue+MySQL的装修公司管理系统

系统展示 管理员后台界面 员工后台界面 系统背景 随着信息技术的快速发展&#xff0c;装修行业正面临数字化转型的关键时刻。传统的装修管理方式存在信息管理混乱、出错率高、信息安全性差等问题&#xff0c;已无法满足现代市场的需求。因此&#xff0c;开发一套高效、便捷的装…

仿《11773手游》源码/手机游戏软件下载门户网站模板/帝国CMS 7.5

帝国CMS 7.5仿《11773手游》源码&#xff0c;手机游戏软件下载门户网站模板。简洁漂亮的手游下载网站模板&#xff0c;采用帝国CMS7.5核心&#xff0c;同步刷新M端。 该模板带同步生成插件&#xff0c;整站干净大气界面漂亮&#xff0c;简单不失简约&#xff0c;模板中的典范&…

css 简单网页布局(一)

1. 三种布局方式 1.1 标准流 1.2 浮动的使用 1.3 简述浮动 1.3.1 浮动三大特性 <style>.out {border: 1px red solid;width: 1000px;height: 500px;}.one {background-color: aquamarine;width: 200px;height: 100px;}.two {background-color: blueviolet;width: 200px;h…

Chromium 中JavaScript Fetch API接口c++代码实现(二)

Chromium 中JavaScript Fetch API接口c代码实现&#xff08;一&#xff09;-CSDN博客 接着上一篇继续介绍调用&#xff0c;上函数堆栈。 1、打开http://192.168.8.1/chfs/shared/test/test02.html 此标签进程ID12484&#xff0c; 2、打开vs附加上此进程ID12484 3、点击页面测…

华为 HCIP-Datacom H12-821 题库 (31)

&#x1f423;博客最下方微信公众号回复题库,领取题库和教学资源 &#x1f424;诚挚欢迎IT交流有兴趣的公众号回复交流群 &#x1f998;公众号会持续更新网络小知识&#x1f63c; 1. 默认情况下&#xff0c;IS-IS Level-1-2 路由器会将 Level-2 区域的明细路由信息发布到Lev…

YOLOv8 基于NCNN的安卓部署

YOLOv8 NCNN安卓部署 前两节我们依次介绍了基于YOLOv8的剪枝和蒸馏 本节将上一节得到的蒸馏模型导出NCNN&#xff0c;并部署到安卓。 NCNN 导出 YOLOv8项目中提供了NCNN导出的接口&#xff0c;但是这个模型放到ncnn-android-yolov8项目中你会发现更换模型后app会闪退。原因…

【STM32 Blue Pill编程实例】-OLED显示HC-SR04超声波测距结果

OLED显示HC-SR04超声波测距结果 文章目录 OLED显示HC-SR04超声波测距结果1、HC-SR04超声波传感器介绍2、硬件准备及接线模块配置3.1 定时器配置3.2 OLED I2C接口配置3.3 HC-SR04引脚配置4、代码实现在本文中,我们将 HC-SR04 超声波传感器与 STM32 Blue Pill 开发板结合使用,并…

Python-函数与数据容器超详解

1.函数的定义 函数是&#xff1a;组织好的、可重复使用的、用来实现特定功能的代码段。它的优点&#xff1a;将功能封装在函数内&#xff0c;可供随时随地重复利用&#xff0c;提高代码的复用性&#xff0c;减少重复代码&#xff0c;提高开发效率 Python函数的定义方式&#…

Perforce演讲回顾(上):从UE项目Project Titan,看Helix Core在大型游戏开发中的版本控制与集成使用策略

日前&#xff0c;Perforce携手合作伙伴龙智一同亮相Unreal Fest 2024上海站&#xff0c;分享Helix Core版本控制系统及其协作套件的强大功能与最新动态&#xff0c;助力游戏创意产业加速前行。 Perforce解决方案工程师Kory Luo在活动主会场&#xff0c;带来《Perforce Helix C…

论文理解【LLM-CV】—— 【MAE】Masked Autoencoders Are Scalable Vision Learners

文章链接&#xff1a;Masked Autoencoders Are Scalable Vision Learners代码&#xff1a;GitHub - facebookresearch/mae发表&#xff1a;CVPR 2022领域&#xff1a;LLM CV一句话总结&#xff1a;本文提出的 MAE 是一种将 Transformer 模型用作 CV backbone 的方法&#xff0c…

新闻推荐系统:Spring Boot的可扩展性

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

csp-j模拟五补题报告

前言 今天第二题文件名把 r 写成 t 了 又跌出前10名了 白丢了好多分 &#xff08;“关于二进制中一的个数的研究与规律”这篇文章正在写&#xff09; 第一题 牛奶(milk) 我的代码&#xff08;AC&#xff09; #include<bits/stdc.h> #define ll long long #define fi …

Acwing 线性DP

状态转移方程呈现出一种线性的递推形式的DP&#xff0c;我们将其称为线性DP。 Acwing 898.数字三角形 实现思路&#xff1a; 对这个三角形的数字进行编号&#xff0c;状态表示依然可以用二维表示&#xff0c;即f(i,j),i表示横坐标&#xff08;横线&#xff09;&#xff0c;j表…

pygame--超级马里奥(万字详细版)

超级马里奥点我下载https://github.com/marblexu/PythonSuperMario 1.游戏介绍 小时候的经典游戏&#xff0c;代码参考了github上的项目Mario-Level-1&#xff0c;使用pygame来实现&#xff0c;从中学习到了横版过关游戏实现中的一些处理方法。原项目实现了超级玛丽的第一个小…

Windows应急响应-Auto病毒

文章目录 应急背景分析样本开启监控感染病毒查看监控分析病毒行为1.autorun.inf分析2.异常连接3.进程排查4.启动项排查 查杀1.先删掉autorun.inf文件2.使用xuetr杀掉进程3.启动项删除重启排查入侵排查正常流程 应急背景 运维人员准备通过windows共享文档方式为公司员工下发软件…