• 使用Redis模拟简单分布式锁,解决单点故障的问题


    需求描述:

        最近做一个项目,项目中有一个功能,每天定时(凌晨1点)从数据库中获取需要爬虫的URL,并发送到对应的队列中,然后客户端监听对应的队列,然后执行任务。如果同时部署多个定时任务节点的话,每个节点都会去查数据库,然后将查到的url发送到队列中,这样的话,客户端就会执行很多重复的任务,如果不同时部署多个节点的话,又存在单点故障的风险。要解决这种类似的问题,可以使用分布式锁来实现,当节点获取到锁的时候就执行任务,没有获取到锁的时候,就不执行任务,这样就解决了多节点同时执行任务的问题,实现分布式锁有多种方法,例如zookeeper,数据库等,今天就用Redis来模拟实现一个简单的分布式锁,来解决这个问题。

    一、新建工程

    本例是基于前面的

    springboot整合H2内存数据库,实现单元测试与数据库无关性

    二、工程结构

    三、添加配置文件

    ########################################################
    ###REDIS (RedisProperties) redis基本配置;
    ########################################################
    # database name
    spring.redis.database=0
    # server host1 单机使用,对应服务器ip
    #spring.redis.host=127.0.0.1  
    # server password 密码,如果没有设置可不配
    #spring.redis.password=
    #connection port  单机使用,对应端口号
    #spring.redis.port=6379
    # pool settings ...池配置
    spring.redis.pool.max-idle=8
    spring.redis.pool.min-idle=0
    spring.redis.pool.max-active=8
    spring.redis.pool.max-wait=-1
    # name of Redis server  哨兵监听的Redis server的名称
    spring.redis.sentinel.master=mymaster
    # comma-separated list of host:port pairs  哨兵的配置列表
    spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26479,127.0.0.1:26579
    
    
    ##########################################################
    ############jpa配置########################################
    #########################################################
    # 服务器端口号  
    server.port=7902
    # 是否生成ddl语句  
    spring.jpa.generate-ddl=false  
    # 是否打印sql语句  
    spring.jpa.show-sql=true  
    # 自动生成ddl,由于指定了具体的ddl,此处设置为none  
    spring.jpa.hibernate.ddl-auto=none  
    # 使用H2数据库  
    spring.datasource.platform=h2  
    # 指定生成数据库的schema文件位置  
    spring.datasource.schema=classpath:schema.sql  
    # 指定插入数据库语句的脚本位置  
    spring.datasource.data=classpath:data.sql  
    # 配置日志打印信息  
    logging.level.root=INFO  
    logging.level.org.hibernate=INFO  
    logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE  
    logging.level.org.hibernate.type.descriptor.sql.BasicExtractor=TRACE  
    logging.level.com.itmuch=DEBUG 
     

    四、定时任务实现

    package com.chhliu.springboot.singlenode.solve.task;
    
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Service;
    
    import com.chhliu.springboot.singlenode.solve.entity.User;
    import com.chhliu.springboot.singlenode.solve.repository.UserRepository;
    
    @Service
    public class ScheduledTasks {
        
        @Autowired
        private UserRepository repository;
        
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        
        private static final String LOCK = "task-job-lock";
        
        private static final String KEY = "tasklock";
        
        //每1分钟执行一次
        @Scheduled(cron = "0 0/1 * * * ?")
        public void reportCurrentByCron() throws InterruptedException{
            boolean lock = false;
            try{
                // 获取锁
                lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
                System.out.println("是否获取到锁:"+lock);
                if(lock){
                    // 如果在执行任务的过程中,程序突然挂了,为了避免程序因为中断而造成一直加锁的情况产生,20分钟后,key值失效,自动释放锁,
                    stringRedisTemplate.expire(KEY, 20, TimeUnit.MINUTES);
                    List<User> users = repository.findAll();
                    if(null != users && !users.isEmpty()){
                        for(User u:users){
                            System.out.println("name:"+u.getName());
                        }
                    }
                    // 模拟长时间任务
                    TimeUnit.MINUTES.sleep(3);
                }else{
                    System.out.println("没有获取到锁,不执行任务!");
                    return;
                }
            }finally{// 无论如何,最终都要释放锁
                if(lock){// 如果获取了锁,则释放锁
                    stringRedisTemplate.delete(KEY);
                    System.out.println("任务结束,释放锁!");
                }else{
                    System.out.println("没有获取到锁,无需释放锁!");
                }
            }
        }
    }
     

    五、测试

    package com.chhliu.springboot.singlenode.solve;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    @EnableScheduling
    public class SinglenodeSolveApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SinglenodeSolveApplication.class, args);
        }
    }
     

    同时启动3个定时任务,模拟多个节点的情况,注意,每次启动的时候,需要修改配置文件中的对应的

    server.port=7902 

    将端口号改成不同。
    测试结果如下:

    节点1:

    是否获取到锁:true
    2017-01-22 14:37:00.099  INFO 1932 --- [pool-2-thread-1] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
    Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.balance as balance3_0_, user0_.name as name4_0_, user0_.username as username5_0_ from user user0_
    name:张三
    name:李四
    name:王五
    name:马六
    任务结束,释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
     

    上面的结果是节点1的执行结果。

    节点2:

    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
     

    节点3:

    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:false
    没有获取到锁,不执行任务!
    没有获取到锁,无需释放锁!
    是否获取到锁:true  // 此时节点1已经释放了锁,节点3获取到了锁
    2017-01-22 14:41:00.072  INFO 5332 --- [pool-2-thread-1] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
    Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.balance as balance3_0_, user0_.name as name4_0_, user0_.username as username5_0_ from user user0_
    name:张三
    name:李四
    name:王五
    name:马六
     

    以上测试结果是5分钟内的情况。
    通过上面的测试,就基本上实现了利用模拟Redis的分布式锁来实现多节点中,同时只有一个节点在运行的目的。

    六、原理分析

    为什么,我们可以用Redis来实现简单的分布式锁的模拟了,这和Redis的一个命令相关,该命令是setnx key value

    该命令的作用是,当往Redis中存入一个值时,会先判断该值对应的key是否存在,如果存在则返回0,如果不存在,则将该值存入Redis并返回1,根据这个特性,我们在程序中,每次都调用setIfAbsent(该方法是setnx命令的实现)方法,来模拟是否获取到锁,如果返回true,则说明该key值不存在,表示获取到锁,如果返回false,则说明该key值存在,已经有程序在使用这个key值了,从而实现了类似加锁的功能。

  • 相关阅读:
    【LeetCode】731. 图像渲染
    【LeetCode】130. 被围绕的区域
    小白之HTTP协议熟悉篇
    Java和js常用表达式
    Mysql主主复制高可用解决方案
    解决vue报错:Module build failed (from ./node_modules/_eslint-loader@2.2.1@eslint-loader/index.js): TypeError: Cannot read property 'range' of null
    centos7使用yum安装jdk并配置jdkhome
    阿里nacos安装及使用指南
    js实现给html固定区域增加水印
    MySQL安装教程
  • 原文地址:https://www.cnblogs.com/zhaoyan001/p/9006570.html
Copyright © 2020-2023  润新知