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

求描述:

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

一、新建工程


本例是基于前面的

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

示例的基础上来实现的。

二、工程结构

三、添加配置文件

[java]  view plain  copy
  1. ########################################################  
  2. ###REDIS (RedisProperties) redis基本配置;  
  3. ########################################################  
  4. # database name  
  5. spring.redis.database=0  
  6. # server host1 单机使用,对应服务器ip  
  7. #spring.redis.host=127.0.0.1    
  8. # server password 密码,如果没有设置可不配  
  9. #spring.redis.password=  
  10. #connection port  单机使用,对应端口号  
  11. #spring.redis.port=6379  
  12. # pool settings ...池配置  
  13. spring.redis.pool.max-idle=8  
  14. spring.redis.pool.min-idle=0  
  15. spring.redis.pool.max-active=8  
  16. spring.redis.pool.max-wait=-1  
  17. # name of Redis server  哨兵监听的Redis server的名称  
  18. spring.redis.sentinel.master=mymaster  
  19. # comma-separated list of host:port pairs  哨兵的配置列表  
  20. spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26479,127.0.0.1:26579  
  21.   
  22.   
  23. ##########################################################  
  24. ############jpa配置########################################  
  25. #########################################################  
  26. # 服务器端口号    
  27. server.port=7902  
  28. # 是否生成ddl语句    
  29. spring.jpa.generate-ddl=false    
  30. # 是否打印sql语句    
  31. spring.jpa.show-sql=true    
  32. # 自动生成ddl,由于指定了具体的ddl,此处设置为none    
  33. spring.jpa.hibernate.ddl-auto=none    
  34. # 使用H2数据库    
  35. spring.datasource.platform=h2    
  36. # 指定生成数据库的schema文件位置    
  37. spring.datasource.schema=classpath:schema.sql    
  38. # 指定插入数据库语句的脚本位置    
  39. spring.datasource.data=classpath:data.sql    
  40. # 配置日志打印信息    
  41. logging.level.root=INFO    
  42. logging.level.org.hibernate=INFO    
  43. logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE    
  44. logging.level.org.hibernate.type.descriptor.sql.BasicExtractor=TRACE    
  45. logging.level.com.itmuch=DEBUG   
四、定时任务实现

[java]  view plain  copy
  1. package com.chhliu.springboot.singlenode.solve.task;  
  2.   
  3. import java.util.List;  
  4. import java.util.concurrent.TimeUnit;  
  5.   
  6. import org.springframework.beans.factory.annotation.Autowired;  
  7. import org.springframework.data.redis.core.StringRedisTemplate;  
  8. import org.springframework.scheduling.annotation.Scheduled;  
  9. import org.springframework.stereotype.Service;  
  10.   
  11. import com.chhliu.springboot.singlenode.solve.entity.User;  
  12. import com.chhliu.springboot.singlenode.solve.repository.UserRepository;  
  13.   
  14. @Service  
  15. public class ScheduledTasks {  
  16.       
  17.     @Autowired  
  18.     private UserRepository repository;  
  19.       
  20.     @Autowired  
  21.     private StringRedisTemplate stringRedisTemplate;  
  22.       
  23.     private static final String LOCK = "task-job-lock";  
  24.       
  25.     private static final String KEY = "tasklock";  
  26.       
  27.     //每1分钟执行一次  
  28.     @Scheduled(cron = "0 0/1 * * * ?")  
  29.     public void reportCurrentByCron() throws InterruptedException{  
  30.         boolean lock = false;  
  31.         try{  
  32.             // 获取锁  
  33.             lock = stringRedisTemplate.opsForValue().setIfAbsent(KEY, LOCK);  
  34.             System.out.println("是否获取到锁:"+lock);  
  35.             if(lock){  
  36.                 // 如果在执行任务的过程中,程序突然挂了,为了避免程序因为中断而造成一直加锁的情况产生,20分钟后,key值失效,自动释放锁,  
  37.                 stringRedisTemplate.expire(KEY, 20, TimeUnit.MINUTES);  
  38.                 List<User> users = repository.findAll();  
  39.                 if(null != users && !users.isEmpty()){  
  40.                     for(User u:users){  
  41.                         System.out.println("name:"+u.getName());  
  42.                     }  
  43.                 }  
  44.                 // 模拟长时间任务  
  45.                 TimeUnit.MINUTES.sleep(3);  
  46.             }else{  
  47.                 System.out.println("没有获取到锁,不执行任务!");  
  48.                 return;  
  49.             }  
  50.         }finally{// 无论如何,最终都要释放锁  
  51.             if(lock){// 如果获取了锁,则释放锁  
  52.                 stringRedisTemplate.delete(KEY);  
  53.                 System.out.println("任务结束,释放锁!");  
  54.             }else{  
  55.                 System.out.println("没有获取到锁,无需释放锁!");  
  56.             }  
  57.         }  
  58.     }  
  59. }  
五、测试

[java]  view plain  copy
  1. package com.chhliu.springboot.singlenode.solve;  
  2.   
  3. import org.springframework.boot.SpringApplication;  
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;  
  5. import org.springframework.scheduling.annotation.EnableScheduling;  
  6.   
  7. @SpringBootApplication  
  8. @EnableScheduling  
  9. public class SinglenodeSolveApplication {  
  10.   
  11.     public static void main(String[] args) {  
  12.         SpringApplication.run(SinglenodeSolveApplication.class, args);  
  13.     }  
  14. }  
同时启动3个定时任务,模拟多个节点的情况,注意,每次启动的时候,需要修改配置文件中的对应的
[java]  view plain  copy
  1. server.port=7902  
将端口号改成不同。
测试结果如下:

节点1:

[java]  view plain  copy
  1. 是否获取到锁:true  
  2. 2017-01-22 14:37:00.099  INFO 1932 --- [pool-2-thread-1] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory  
  3. 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_  
  4. name:张三  
  5. name:李四  
  6. name:王五  
  7. name:马六  
  8. 任务结束,释放锁!  
  9. 是否获取到锁:false  
  10. 没有获取到锁,不执行任务!  
  11. 没有获取到锁,无需释放锁!  
上面的结果是节点1的执行结果。

节点2:

[java]  view plain  copy
  1. 是否获取到锁:false  
  2. 没有获取到锁,不执行任务!  
  3. 没有获取到锁,无需释放锁!  
  4. 是否获取到锁:false  
  5. 没有获取到锁,不执行任务!  
  6. 没有获取到锁,无需释放锁!  
  7. 是否获取到锁:false  
  8. 没有获取到锁,不执行任务!  
  9. 没有获取到锁,无需释放锁!  
  10. 是否获取到锁:false  
  11. 没有获取到锁,不执行任务!  
  12. 没有获取到锁,无需释放锁!  
节点3:

[java]  view plain  copy
  1. 是否获取到锁:false  
  2. 没有获取到锁,不执行任务!  
  3. 没有获取到锁,无需释放锁!  
  4. 是否获取到锁:false  
  5. 没有获取到锁,不执行任务!  
  6. 没有获取到锁,无需释放锁!  
  7. 是否获取到锁:false  
  8. 没有获取到锁,不执行任务!  
  9. 没有获取到锁,无需释放锁!  
  10. 是否获取到锁:true  // 此时节点1已经释放了锁,节点3获取到了锁  
  11. 2017-01-22 14:41:00.072  INFO 5332 --- [pool-2-thread-1] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory  
  12. 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_  
  13. name:张三  
  14. name:李四  
  15. name:王五  
  16. name:马六  
以上测试结果是5分钟内的情况。
通过上面的测试,就基本上实现了利用模拟Redis的分布式锁来实现多节点中,同时只有一个节点在运行的目的。

六、原理分析

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值