1、场景说明
- 定时任务在服务启动的时候,需要加载到定时任务容器中,保证服务重启后未执行的定时任务仍能够执行,但是生产环境服务是分布式部署,每个节点上的服务都会将定时任务加载到自己的定时任务容器中,导致同一个定时任务被多次加载、执行。
2、问题分析
- 可以使用分布式锁,在服务启动的时候获取锁,获取到锁的服务将定时任务添加到定时任务容器中,没有获取到锁的不会将定时任务添加到定时任务容器中,我们只要保证只有一个服务获取到锁就可保证定时任务只被一个服务加载。
- 分布式锁选用redis实现分析
- redis分布式锁需要添加过期时间;不添加过期时间,key一直存在redis中,以后服务重启都无法获取到锁,无法加载需要执行的定时任务
- redis分布式锁,如果过期时间过短是1分钟,第一个节点部署服务时间1分钟,再过1分钟后第二节点部署服务时,key已经过期,第二个节点上的服务仍然可以获取到锁,会重复加载定时任务。
- redis分布式锁,如果过期时间过长是10分钟,第一个节点部署服务时间1分钟,第二个节点部署服务时间也是1分钟,这时发现第一个节点上的服务启动异常,需要等待redis中的key10分钟过期后,在第一个节点重新部署服务才能保证定时任务能够重新被加载。
- 分布式锁选用zookeeper实现分析
- zk分布式锁不用添加过期时间,某一节点服务启动后获取到锁,则一直持有锁,没有获取到锁的服务,则不会加载定时任务。
- zk分布式锁创建的是临时节点,服务结束(会话结束)临时节点会自动删除。
3、zk分布式锁的实现
-
引入依赖
注意:
-
maven依赖要和部署的zookeeper版本匹配才能正常使用
-
我的zookeeper服务器的版本是3.4.7,依赖的spring-integration-zookeeper可以选择5.0.0版本
<dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-zookeeper</artifactId> <version>5.0.0.RELEASE</version> </dependency>
-
如果版本不匹配(例如选用5.3.2.RELEASE版本)会报错
# 错误信息 Unable to read additional data from server sessionid 0x179c14deb3d001a, likely server has closed socket, closing socket connection and attempting reconnect 2021-05-31 22:08:25.593 ERROR 22516 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for /lock/_c_464eeec5-3622-4787-aa70-9f1eabf74703-lock-] with root cause org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for /lock/_c_464eeec5-3622-4787-aa70-9f1eabf74703-lock-
-
-
zookeeper相关配置
# zookeeper分布式锁配置 #重试次数 curator.retryCount = 5 #重试间隔时间 curator.baseSleepTimeMs = 1000 # zk地址,集群用逗号分隔 curator.connectString = 192.168.10.111:2181 #session超时时间(ms) curator.sessionTimeoutMs = 60000 #连接超时时间(ms) curator.connectionTimeoutMs = 15000
-
zookeeper配置类,将curator注入到spring容器中
-
@Bean(initMethod = “start”)中的initMethod指定初始化之后调用的方法,这样就不用手动去写curatorFramework.start()
-
@value中的属性后面冒号后面的值,表示默认值,如果从配置文件中没有读取到属性就使用默认属性
-
代码
@Configuration public class ZookeeperConfig { @Value("${curator.retryCount:5}") private int retryCount; @Value("${curator.baseSleepTimeMs:1000}") private int baseSleepTimeMs; @Value("${curator.connectString:192.168.10.110:2181}") private String connectString; @Value("${curator.sessionTimeoutMs:60000}") private int sessionTimeoutMs; @Value("${curator.connectionTimeoutMs:15000}") private int connectionTimeoutMs; @Bean(initMethod = "start") public CuratorFramework curatorFramework() { return CuratorFrameworkFactory.newClient( connectString, sessionTimeoutMs, connectionTimeoutMs, new RetryNTimes(retryCount, baseSleepTimeMs)); } }
-
-
获取到zookeeper锁的服务执行业务代码
-
lock.acqire(10, TimeUtil.SECONDS)表示获取锁,如果10秒没有获取到,放弃获取
-
获取到zookeeper锁,则执行业务代码,否则不执行业务代码
-
获取到锁的服务不主动释放锁,会话结束(服务停止)自动释放锁
-
模拟业务的代码
@RestController public class StreamTest { @Autowired private CuratorFramework curatorFramework; @RequestMapping("/hello") public void testZK() throws Exception { //添加zookeeper分布式锁,保证只有一台主机能够添加定时任务,防止定时任务重复执行 String lockPath = "/lock"; InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath); System.out.println("请求进来了。。。。。。。。。。。。"); //等待10秒获取不到锁就不再获取 if (lock.acquire(10, TimeUnit.SECONDS)) { //业务代码 System.out.println(Thread.currentThread().getName() + "获取到zk锁。。。。。。。。"); } System.out.println("请求结束了。。。。。。"); } }
-
-
服务器上查看临时节点
# 进入到zookeeper的bin目录下,每个人服务安装zookeeper的地址不同 cd /home/zookeeper-3.4.7/bin/ # 查看bin目录下的文件 ll # 使用zkCli.sh连接到zookeeper服务端 zkCli.sh -server 192.168.10.111:2181 #查看根路径下的节点 ls / # 查看我们创建的/lock下的临时节点 ls /lock