Java --- redis实现分布式锁

news2024/11/25 22:39:22

目录

一、锁的种类

 二、分布式锁具备的条件与刚需

 三、springboot+redis+ngnix单机实现案例

 四、Nginx配置负载均衡

 4.1、修改nginx配置文件

 4.2、执行启动命令

 4.3、启动微服务程序测试

 五、使用redis分布式锁

  5.1、方法递归重试

 5.2、自旋方式

 5.3、添加key过期时间,防死锁

 5.4、防止误删除

 5.6、使用Lua脚本优化

5.7、可重入锁

5.7.1、隐式锁(synchronized)

 5.7.2、显式锁(Lock)

5.7.3、redis的Map类型替代可重入锁的计数问题

5.8、自动续期

一、锁的种类

1、单机版同一个JVM虚拟机内,synchronized或者Lock接口 

2、分布式多个不同JVM虚拟机,单机的线程机制不再起作用,资源类不在同一服务器上,不在共享。

 二、分布式锁具备的条件与刚需

独占性:

OnlyOne,任何时刻只能有且仅有一个线程持有。

高可用:

在redis集群环境下,不能因为某一个节点挂掉而出现获取锁和释放锁失败的情况,高并发请求下,依旧性能好用。

防死锁:

杜绝死锁,必须有超时控制机制或者撤销操作,必须有最终解决方案。

不乱抢:

自己的锁只能自己释放,不能释放不属于自己的锁。

重入性:

同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

 三、springboot+redis+ngnix单机实现案例

场景:

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求 (防止关键业务出现并发攻击)

 采用微服务创建:

 test1与test2两台pom导入jar一致

    <dependencies>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.13</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

 springboot配置文件

server.port=8081
spring.application.name=redis7test1
#swagger2
spring.swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
#redis配置
spring.redis.database=0
spring.redis.host=192.168.200.110
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

配置类

@Configuration
public class RedisConfig {
    /**
     * redis序列化配置类
     *
     */
    @Bean
    public RedisTemplate<String , Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
        RedisTemplate<String , Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
@Component
@EnableSwagger2
public class SwaggerConfig {
    @Value("${spring.swagger2.enabled}")
    private Boolean enabled;
    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.cjc.redis7test1"))
                .paths(PathSelectors.any())
                .build();
    }
    public ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档"+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot+redis整合,有问题联系客服小丽:123456789")
                .version("1.0")
                .termsOfServiceUrl("yyyyyyyyy")
                .build();
    }
}

service层

@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    public static final String IN_KEY = "inventory";

    private Lock lock = new ReentrantLock();
    @Override
    public String sale() {
        String resultMessage = "";
        //加锁
        lock.lock();
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                 resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //解锁
            lock.unlock();
        }
        return resultMessage;
    }
}

controller层

@RestController
@Slf4j
@Api(tags = "redis分布式锁测试")
public class InventoryController {
    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("扣减库存,一次卖一个")
    @RequestMapping(value = "/sale",method = RequestMethod.GET)
    public String sale(){
        return inventoryService.sale();
    }
}

上述代码在test2上拷贝,修改端口号

 四、Nginx配置负载均衡

 上面的代码在分布式部署后,单机锁将会出现超卖现象,需要分布式锁。

 4.1、修改nginx配置文件

cd /usr/local/nginx/conf目录下的nginx.conf文件

 4.2、执行启动命令

在/usr/local/nginx/sbin目录下启动

启动命令:

./nginx -c /usr/local/nginx/conf/nginx.conf

 关闭

./nginx -s stop

重启

./ngnix -s reload

 4.3、启动微服务程序测试

 手动测试,程序正常

 使用jmeter工具进行压测

 

 原因如下:

在单机环境下,可以使用synchronized或lock来实现

在分布式系统中,因为竞争的线程可能不在同一个节点上(即同一个jvm中)所以锁就失效了,就需要利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。

结论:

单机版加锁配合nginx和jmeter压测后,不满足高并发分布式锁的性能要求

 五、使用redis分布式锁

  5.1、方法递归重试

    @Override
    public String sale(){
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        //没有抢到锁将进行重试
        if (!flag){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            sale();
        }else {
            //拿到锁进行业务处理
            try {
                //查询redis里的库存
                String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
                //判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //卖出商品扣减库存
                if (inventoryNumber > 0){
                    //存入redis
                    stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                     resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                    log.info(resultMessage + "\t服务端口号:{}",port);
                }else {
                    resultMessage = "商品已售空";
                }
            } finally {
               stringRedisTemplate.delete(key);
            }
        }
        return resultMessage;
    }

 使用方法递归重试测试通过,但并发量一高,容易出现StackOverflowError,高并发唤醒后推荐使用while判断,不推荐使用。

 5.2、自旋方式

@Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return resultMessage;
    }
上面代码在程序挂掉的情况下且没有走到finally这块,由于该key没有设置过期时间,导致会没办法删除。

 5.3、添加key过期时间,防死锁

@Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            stringRedisTemplate.delete(key);
        }
        return resultMessage;
    }

问题当A线程处理业务时间超过过期时间,B线程也建立了一道锁,会导致A线程误删除B线程的锁。

 5.4、防止误删除

  @Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //删除锁时,判断该锁是否是同一客户端的
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
                stringRedisTemplate.delete(key);
            }
        }
        return resultMessage;
    }

 在判断与删除时不是一个原子操作,可能会因为一些特性原因删除失败,需要使用Lua脚本进行优化

 5.6、使用Lua脚本优化

    @Override
    public String sale() {
        String resultMessage = "";
        String key = "redisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        //使用自旋代替递归,使用while进行判断
        //加锁和过期时间设置必须是同一行,保证原子性
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,20L,TimeUnit.SECONDS)){
            //暂停20毫秒,进行递归重试,
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //拿到锁进行业务处理
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //使用lua脚本的redis分布式调用
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else" +
                    "\treturn 0 end";
            stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);

        }
        return resultMessage;
    }

上述代码不满足可重入性 

5.7、可重入锁

在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(锁对象是同一对象),不会因为之前已经获取过还没有释放而阻塞。Java中ReentrantLoak和synchronized都是可重入锁,可重入锁的一个优点是一定程度上避免了死锁。

5.7.1、隐式锁(synchronized)

    Object o =  new Object();
    @Test
    void contextLoads() {

        new Thread(()->{
            synchronized (o){
                System.out.println(Thread.currentThread().getName() + "\t外层调用");
                synchronized (o){
                    System.out.println(Thread.currentThread().getName() + "\t中层调用");
                    synchronized (o){
                        System.out.println(Thread.currentThread().getName() + "\t内层调用");

                    }
                }
            }
        },"a").start();
    }
    @Test
   void entry(){
      m1();
   }
    private synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + "\t外层调用");
        m2();
    }
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "\t中层调用");
        m3();
    }
    private synchronized void m3() {
        System.out.println(Thread.currentThread().getName() + "\t内层调用");
    }

 原理:

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为0,那么它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需要将锁对象的计数器减1,计数器为零代表锁已被释放。

 5.7.2、显式锁(Lock)

    @Test
    void lockTest(){
        Lock lock = new ReentrantLock();
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t中层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        },"a1").start();
        try {
            TimeUnit.MILLISECONDS.sleep(3);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t内层调用");
            } finally {
                lock.unlock();
            }
        },"a2").start();
    }

5.7.3、redis的Map类型替代可重入锁的计数问题

加锁Lua脚本:

if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
   redis.call('hincrby',KEYS[1],ARGV[1],1)
   redis.call('expire',KEYS[1],ARGV[2])
   return 1
else 
   return 0
end

解锁Lua脚本:

if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
   return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then
       return redis.call('del',KEYS[1])
else 
   return 0
end

/**
 * 自定义redis分布式锁
 */
@Slf4j
public class MyRedisLock implements Lock {

   private StringRedisTemplate stringRedisTemplate;
   private String lockName;
   private String uuidValue;
   private Long expireTime;

    public MyRedisLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuid) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuid + ":" + Thread.currentThread().getId();
        this.expireTime = 50L;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L){
            String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +
                    "redis.call('expire',KEYS[1],ARGV[2])    " +
                    "return 1  " +
                    "else   " +
                    "return 0 " +
                    "end";
            log.info("lockName:{},uuidValue:{}",lockName,uuidValue);
            //加锁失败重试
            Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
            while (!execute){
               TimeUnit.MILLISECONDS.sleep(60);
            }
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then\t" +
                "return nil\t" +
                "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then\t" +
                "return redis.call('del',KEYS[1])\t" +
                "else\t" +
                "return 0\t" +
                "end";
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime));
        if (flag == null){
            throw new RuntimeException("this lock doesn't exists");
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }
@Component
public class DistributedLockFactory {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuid;

    public DistributedLockFactory() {
        this.uuid = IdUtil.simpleUUID();
    }

    public Lock getDistributedLock(String lockType){
        if (lockType == null){
            return null;
        }
        if (lockType.equalsIgnoreCase("REDIS")){
            lockName = "redisLock";
            return  new MyRedisLock(stringRedisTemplate,lockName,uuid);
        }
        if (lockType.equalsIgnoreCase("MySQL")){
            this.lockName = "mysqlLock";
            //TODO 完善
            return  null;
        }
        return null;
    }
}
@Override
    public String sale() {
        String resultMessage = "";
        //加锁
        Lock redisLock = distributedLockFactory.getDistributedLock("REDIS");
        redisLock.lock();
        try {
            //查询redis里的库存
            String result = stringRedisTemplate.opsForValue().get(IN_KEY + 1);
            //判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //卖出商品扣减库存
            if (inventoryNumber > 0){
                //存入redis
                stringRedisTemplate.opsForValue().set(IN_KEY + 1,String.valueOf(--inventoryNumber));
                 resultMessage = "成功卖出商品,库存剩余:" + inventoryNumber;
                log.info(resultMessage + "\t服务端口号:{}",port);
            }else {
                resultMessage = "商品已售空";
            }
        } finally {
            //解锁
            redisLock.unlock();
        }
        return resultMessage;

    }

5.8、自动续期

Lua脚本:

if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then 
  return redis.call('expire',KEYS[1],ARGV[2]) 
else 
  return 0 
end

    public void renewExpire(){
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then\t" +
                "return redis.call('expire',KEYS[1],ARGV[2])\t" +
                "else\t" +
                "return 0\t" +
                "end";
        //定期扫描
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
                if (execute){
                    renewExpire();
                }
            }
        },(this.expireTime * 1000/3));
    }

 

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

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

相关文章

Vue3如何按需引入Element Plus以及定制主题色

1.首先使用指令进行安装 npm install element-plus --save 2.安装按需引入另外两个插件 npm install -D unplugin-vue-components unplugin-auto-import 3.在vite.config.js文件引入以下内容 import { fileURLToPath, URL } from node:urlimport { defineConfig } from vite i…

CloudFlare系列--自定义CDN节点的IP

原文网址&#xff1a;CloudFlare系列--自定义CDN节点的IP_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍CloudFlare如何手动指定CDN为CloudFlare的IP地址。 为什么手动指定CDN IP&#xff1f; 自选节点非常重要&#xff0c;原因如下&#xff1a; 国内访问不同CDN节点的速…

Linux vim光标移动/退出命令/撤退操作/文本查找 等命令大全

1 什么是vim&#xff1f; vim是Linux环境下一款强大、高度可定制的文本编辑工具。能够编辑任何的ASCII格式文件&#xff0c;对内容进行创建、查找、替换、修改、删除、复制、粘贴等操作。编写文件时&#xff0c;无需担心目标文件是否存在&#xff0c;若不存在则会自动在内存中…

老Q魔改MACD:拒绝大幅回撤,威力比原版强太多了!

看过老Q历史文章的股友都知道,MACD是一个非常经典且依旧奋战在第一线的顶流指标。我们之前也目前主流通用的参数版本在沪深300上做了回测,17年来获得了累计365%的收益。 然而,整个沪深300大盘在这17年里也涨了超过300%,也就是说,我们的策略也仅仅比拿着不动好上一丢丢而已…

JDK8中的新特性(Lambda、函数式接口、方法引用、Stream)

文章目录 1. Java8新特性&#xff1a;Lambda表达式1.1 关于Java8新特性简介1.2 冗余的匿名内部类1.3 Lambda 及其使用举例1.4 语法1.5 关于类型推断 2. Java8新特性&#xff1a;函数式(Functional)接口2.1 什么是函数式接口2.2 如何理解函数式接口2.3 举例2.4 Java 内置函数式接…

高压功率放大器在换流阀冷却系统均压电极结垢超声导波中的应用

实验名称&#xff1a;换流阀冷却系统均压电极结垢超声导波检测方法研究 研究方向&#xff1a;无损检测 测试目的&#xff1a; 为了探究超声导波检测的灵敏度&#xff0c;本文构建了换流阀冷却系统均压电极结垢检测模型&#xff0c;详细分析了不同厚度水垢与声波信号的交互过…

【c++】哈希---unordered容器+闭散列+开散列

1.unordered系列关联式容器 在C98中&#xff0c;STL提供了底层为红黑树结构的一系列关联式容器&#xff0c;在查询时效率可达到 logN&#xff0c;即最差情况下需要比较红黑树的高度次&#xff0c;当树中的节点非常多时&#xff0c;查询效率也不理想。最好的查询是&#xff0c;进…

FPGA开发基本流程详解

FPGA是一种可编程逻辑器件&#xff0c;与传统的硬连线电路不同&#xff0c;它具有高度的可编程性和灵活性。FPGA的设计方法包括硬件设计和软件设计两部分&#xff0c;硬件设计包括FPGA芯片电路、存储器、输入输出接口电路等等&#xff0c;软件设计则是HDL程序开发&#xff0c;以…

openCV 第四篇 角点检测、图像特征、图片拼接

本文原本打算直接简单介绍一下harris和sift&#xff0c;之后进行特征匹配&#xff0c;来一波图像拼接。 想来想去还是先介绍下原理吧&#xff0c;虽然没人看QAQ。可以直接点击右侧目录跳转到代码区。 本文可以完成&#xff1a; 角点检测 和 图像特征提取(就几行代码) 以及进…

Win32子窗口创建,子窗口回调函数,消息堆栈,逆向定位子窗口消息处理过程

本专栏上一篇文章中我们讲解了Win32程序入口识别&#xff0c;定位回调函数&#xff0c;具体事件处理的定位&#xff0c;这一章节中我们来讲解一下子窗口的创建&#xff0c;子窗口的回调函数&#xff0c;并且逆向分析子窗口消息处理过程。 文章目录 一.子窗口按钮的创建- 创建子…

charge pump的分析与应用

春节前最后一更&#xff0c;提前祝大家新春快乐&#xff0c;阖家安康&#xff0c;工作顺利&#xff01; 定义&#xff1a; 电荷泵是利用电容的充放电来实现电压的转换的&#xff0c;输入回路和输出回路轮流导通。通过调节占空比来调节输出电压。 它们能使输入电压升高或降低&…

基于PyQt5连接本地SQLite实现简单人力资源管理系统

人力资源管理系统 使用环境&#xff1a;Python3.86 PyQt5.15.4 sqlite3 记录一下最近学校举办的一个程序设计比赛&#xff0c;题目是实现一个简单的人力资源管理系统&#xff0c;文末有效果展示 我认为程序是面向人类而不是面向机器的&#xff0c;所以我使用了PyQt5封装了一…

SpringCloud源码分析 (Eureka-Server-处理客户端续约请求) (七)

文章目录 1.处理客户端续约请求1.1 InstanceResource.renewLease()1.2 InstanceRegistry.renew()1.3 PeerAwareInstanceRegistryImpl.renew()1.4 AbstractInstanceRegistry.renew()1.6 PeerAwareInstanceRegistryImpl.replicateToPeers()1.7 PeerEurekaNode.headbeat() 1.处理客…

大数据Doris(二十二):Rollup物化索引创建与操作

文章目录 Rollup物化索引创建与操作 一、创建测试表 二、创建Rollup物化索引表

岗位少,竞争激烈,这是今年软件测试就业的真实写照,也是所有岗位的真实写照。

前两天跟一个HR朋友聊天&#xff0c;她表示刚在boss上发布了一个普通测试岗位&#xff0c;不到一小时竟然收到了几百份简历。而且简历质量极高&#xff0c;这是往年不敢想象的。岗位少&#xff0c;竞争激烈&#xff0c;这是今年软件测试就业的真实写照&#xff0c;也是所有岗位…

若依框架快速开发项目(避坑超详细)

若依框架快速开发项目&#xff08;避坑超详细&#xff09; 初衷&#xff1a; 若依框架使用及其普遍&#xff0c;是一个非常优秀的开源框架&#xff0c;框架本身的权限系统&#xff0c;字典设置以及相关封装&#xff0c;安全拦截相当完善&#xff0c;本人受益匪浅&#xff0c;学…

Python进阶实际应用开发实战(一)

目录 原型设计和环境环境设置创建新项目 原型设计和环境 原书第一章内容 环境设置 对于一个项目我们需要安装库并管理依赖项&#xff0c;这意味着需要有一个虚拟环境。我们使用pipenv来指定依赖项。 python -m pip install --user pipenv在命令行中启动Python脚本的时候&am…

分布式锁概念

什么是分布式锁 方案一&#xff1a;SETNX EXPIRE 方案二&#xff1a;SETNX value值是&#xff08;系统时间过期时间&#xff09; 方案三&#xff1a;使用Lua脚本(包含SETNX EXPIRE两条指令) 方案四&#xff1a;SET的扩展命令&#xff08;SET EX PX NX&#xff09; 方案五…

chatgpt赋能Python-aidlearning安装pycharm

Aid Learning: 如何安装PyCharm PyCharm是一款由JetBrains开发的用于Python编程的集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的编辑器和调试工具&#xff0c;可以帮助开发者更高效地编写Python代码。本文将介绍如何安装PyCharm。 安装前准备工作 在安装PyCharm…