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

news2024/11/28 5:32:23

1.前言

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

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

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

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

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

    @Test
    void 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 acquired
    if (ttl == null) {
        return;
    }

    //    订阅
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    commandExecutor.syncSubscription(future);

    try {
        while (true) {
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            if (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.coloradmin.cn/o/1259334.html

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

相关文章

大连大学2023年11月程序设计竞赛(同步赛)

B、爆wa种子!&#xff08;数学&#xff09; 一、题目要求 链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 爆wa种子发现了上次玩游戏时你和妙wa种子的py交易&#xff0c;所以他要求这次玩游戏你来当爆wa种子的枪手&#xff0c;为他写个程序…

Selenium 连接到现有的 Google Chrome 示例

python 3.7 selenium 3.14.1 urllib3 1.26.8 Google Chrome 119.0.6045.160 (64位) chromedriver.exe 119.0.6045.105(win32) 1 Google Chrome 添加参数 "--remote-debugging-port9222" 2 测试效果(chromedriver.exe 要和 Google Chrome 版本…

day65

今日回顾内容 web应用 HTTP协议 web应用 一、什么是web应用程序 Web应用程序是一种可以通过Web访问的应用程序&#xff0c;程序的最大好处是用户很容易访问应用程序&#xff0c;用户只需要有浏览器即可&#xff0c;不需要再安装其他软件 对于传统的应用软件来说&#xff0c;…

【备忘录】软件记录

Anaconda 虚拟环境 创建Python环境 Spyder Python程序编辑 Jupyter Notebook 交互式开发环境

【虹科干货】ntopng如何将漏洞扫描与流量监控相结合,以提高网络安全性

ntopng为人所知的“身份”是被动流量监控。然而&#xff0c;如今的ntopng6.0也进化出主动监控功能来&#xff0c;漏洞扫描功能便是其中一个。那么漏洞扫描功能是什么&#xff1f;其独特之处是什么&#xff1f;用户该如何使用&#xff1f; 新的漏洞扫描和CVE支持&#xff0c;可…

TUP通信

一&#xff0c;概括 二&#xff0c;常用方法 三&#xff0c; 实现步骤&#xff08;一发一收&#xff09; 四&#xff0c;案例&#xff08;一接一收&#xff09; &#xff08;1&#xff09;&#xff0c;客户端 &#xff08;2&#xff09;&#xff0c;服务端 &#xff08;3&…

老板说我能力不行,怎么办?

大家好&#xff0c;我是鱼皮&#xff0c;今天分享一个很现实的职场问题 —— 如果老板说你的能力不行&#xff0c;怎么办&#xff1f; 该问题源于 编程导航星球 内的鱼友提问&#xff0c;原问题如下&#xff1a; 鱼友提问 &#x1f41f;&#xff0c;我是 23 届毕业生&#x…

探究Kafka原理-6.CAP理论实践

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

华为智能手表独立导航,一呼即应轻松畅行

PetalMaps 手表独立导航&#xff0c;一声令下唤醒导航&#xff0c;打造了智慧的语音交互唤醒体验功能。导航时&#xff0c;语音播报、变道震动提醒功能&#xff0c;让您尽情体验腕上导航乐趣&#xff0c;同时又能安全抵达目的地。

如何在外远程访问本地NAS威联通QNAP?

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、Cpolar杂谈 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 威联通安装cpolar内网穿透二. 内网穿透2.1 创建隧道2.2 测试公网远程访问 三.…

51单片机的智能窗帘系统【含proteus仿真+程序+报告+原理图】

1、主要功能 该系统由AT89C51单片机LCD1602显示模块DS18B20温度模块DS1302时间模块光敏传感器步进电机蓝牙等模块构成。适用于智能窗帘、智能门窗家具等相似项目。 可实现基本功能: 版本一&#xff1a; 1、LCD1602实时显示北京时间、环境温度、光照强度、手动/自动控制等信息…

vatee万腾的数字探险:Vatee科技创新的未知征程

在科技风潮的巅峰&#xff0c;Vatee万腾如一艘科技探险的航船&#xff0c;勇敢地驶向未知的数字化征程。 Vatee万腾在数字探险的过程中展现出征服未知领域的坚定决心。他们不满足于现状&#xff0c;而是积极地寻找和探索那些尚未被揭示的数字化领域。这种决心使得Vatee能够在科…

眼见非实-MISC-bugku-解题步骤

——CTF解题专栏—— 题目信息&#xff1a; 题目&#xff1a;眼见非实 作者&#xff1a;harry 提示&#xff1a;无 解题附件&#xff1a; 解题思路&#xff1a; 眼见非实&#xff1f;&#xff1f;&#xff1f;难道要用手摸一下&#xff1f;&#xff08;开玩笑.jpg&#xff…

第一篇:快速入门

简介 本篇文章主要目的教你如何快速的理解、掌握cocos shader的相关知识&#xff0c;并附加实践案例。 shader 我们可以理解为是一种在图形渲染过程中控制像素颜色的过程&#xff0c;通常用来创建各种视觉效果。如光照、阴影、扭曲等。 Material&#xff08;材质&#xff0…

safari浏览器,直接安装ipa文件

蒲公英二维码方法 个人开发者账号发布证书AD-hoc 描述文件蒲公英上传链接通过苹果safari 浏览器下载IPA包 浏览器下载方法 前置条件 1.下载 ipa 包的设备的 uuid 已加入 苹果测试设备列表如何添加到测试列表 2.web 服务, 文件服务. 3.需要AD-hoc 描述文件 添加链接描述 1.创…

华大基因在合规管理、高质量发展方面将迈上新的台阶

今年6月&#xff0c;华大基因顺利通过了国际领先标准、测试及认证机构BSI的严格审核&#xff0c;获得GB/T 35770-2022 / ISO 37301:2021合规管理体系认证证书&#xff0c;成为行业内率先获此国际认证的企业。 ISO 37301合规管理体系认证是国际通用的合规管理体系认证标准&…

【Java Spring】SpringBoot 日志系统

文章目录 一、Spring Boot 日志系统1.1 Spring Boot 日志框架1.2 自定义日志打印1.3 日志级别设置1.4 日志持久化1.5 lombok 简化日志输出 一、Spring Boot 日志系统 1.1 Spring Boot 日志框架 SLF4J 和 logback都是spring boot内置的日志框架&#xff0c;开发者只负责调用对…

opencv-医学图像预处理

医学图像预处理通常需要针对特定任务和数据集的特点进行定制。以下是一些常见的医学图像预处理步骤&#xff0c;可以使用OpenCV以及其他相关库来实现&#xff1a; 导入相关的库 import cv2 import matplotlib.pyplot as plt1. 读取图像 image cv2.imread(r"C:\Users\m…

YOLOv8-Seg改进:自适应改变核大小卷积AKConv,效果优于标准卷积核和DSConv |2023.11月最新成果

🚀🚀🚀本文改进: AKConv 中,通过新的坐标生成算法定义任意大小的卷积核的初始位置。 为了适应目标的变化,引入了偏移量来调整每个位置的样本形状。 此外,我们通过使用具有相同大小和不同初始采样形状的 AKConv 来探索神经网络的效果。 AKConv 通过不规则卷积运算完成…

8.二维数组——将一个二维数组行和列的元素互换,存到另一个二维数组中。

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码 前言 本系列为二维数组编程题&#xff0c;点滴成长&#xff0c;一起逆袭。 一、题目描述 将一个二维数组行和列的元素互换&#xff0c;存到另一个二维数组中。 二、题目分析 三、解题 程序运行代码 #incl…