需求背景

在JAVA应用开发过程中,越来越多的项目采用了微服务架构,而采用微服务架构最直接作用是可以实现业务层解耦,有利于研发团队可以从业务层面进行划分,比如某几个人的小团队负责某几个微服务,总之,从业务角度来讲的话,让专业的人做专业的事。而同时由于部署了微服务后,经常需要保证业务的高可用,就有了同一服务多机部署的概念,而有的服务在任务处理的时候,可能需要保证任务处理的顺序性,在同一服务多机的时候,保证任务的顺序性其实是一个比较复杂的问题,比如说经常会用的邮件通知服务,邮件通知时,如果不按照业务的顺序进行通知的话,可能会造成一定错误。(比如一个告警邮件:第一个邮件:告警,第二个邮件解除告警,在这种情况下,如果顺序错乱会导致业务上造成误会甚至错误)

现实案例

某某产品中的邮件服务,需要多机部署,而同时需要保证邮件发送的顺序性。

目前架构中邮件服务的实现基本原理:各个服务根据需要,将需要发送的邮件内容,提交到redis队列,再由邮件服务进行轮询获取发送。

难点分析

但目前没有进行多机部署,无法保证服务的高可用,需要对其进行多机部署改造。而多机部署后,又涉及到邮件的实际发送顺序问题,所以在高可用的同时,仍需要保证业务的单一顺序性。对此在多机的同时,为其增加主备的功能,在多机的情况下,通过选主的方式选出主节点即实际工作节点,当主节点发生宕机的时候,再进行一次选主,选出另外一个工作节点。对于这种业务需要保证单一顺序的服务模块,通过主备的方式进行实现。

主备设计

由于目前系统中没有引入zookpeer分布式协调工具,所以对于选主目前通过redis来进行实现。

  1. 对于每一个JVM进程为其分配唯一的NODE_ID,启动后通过heartbeat机制定时(间隔20S)地在redis设值(key:jvm_process_NODE_ID_heartbeat value:NODE_ID ttl:30S)
  2. 系统启动后,主动地定时地去争取master lock,并获取其master状态;
  3. 当前进程如果争取master lock成功,则将自身的NODE_ID,设为value,并将过期时间设为30S;
  4. 如果master lock已被占有,根据其NODE_ID进行判断 ,如果是自身,则延长key存活时间,如果不是自身,则获取其value,判断value所指的NODE_ID的节点是否还存活(通过心跳去检查另外的节点),如果不存活,直接通过cas操作将其改为当前进程的NODE_ID,cas操作成功则抢主成功,反之则失败。
  5. 当获取节点状态(要么master 要么slave)后,则触发master-slave事件。

pom文件

这里需要用到redis,引入以下依赖

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.4</version>
</dependency>

心跳机制

通过java进程定时地去设值redis key的方式去维持进程的心跳,这里为了方便redis的操作,使用了redisson client工具类。

为了方便地使用通过单例的方式进行获取使用,同时将引入到spring 中,交由spring管理并初始化

public class JVMProcessHeartbeat {
private static final Logger log = LoggerFactory.getLogger(JVMProcessHeartbeat.class);
/**
* 进程唯一id
*/
private static final String NODE_ID = SystemConstant.NODE_ID; /**
* 维护心跳的key值格式
*/
private static final String FORMAT = "jvm_process_%s_heartbeat"; /**
* 根据NODE_ID生成当前进程的key
*/
private static final String HEARTBEAT_KEY = String.format(FORMAT, NODE_ID); /**
* redis操作工具
*/
private RedissonClient redissonClient; /**
* 定时维持心跳的线程池
*/
private ScheduledExecutorService scheduledExecutorService; /**
* 单例模式设计心跳
*/
private static JVMProcessHeartbeat jvmProcessHeartbeat; /**
* DCL单例实现
*
* @param redissonClient
* @return
*/
public static JVMProcessHeartbeat getInstance(RedissonClient redissonClient) {
if (jvmProcessHeartbeat == null) {
synchronized (JVMProcessHeartbeat.class) {
if (jvmProcessHeartbeat == null) {
jvmProcessHeartbeat = new JVMProcessHeartbeat(redissonClient);
}
}
}
return jvmProcessHeartbeat;
} private JVMProcessHeartbeat(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
init();
} /**
* 初始化心跳维护线程
*/
private void init() {
BasicThreadFactory basicThreadFactory = new BasicThreadFactory.Builder().namingPattern("heartbeat").build();
scheduledExecutorService = new ScheduledThreadPoolExecutor(1, basicThreadFactory);
scheduledExecutorService.scheduleAtFixedRate(() -> {
RBucket<String> bucket = redissonClient.getBucket(HEARTBEAT_KEY);
bucket.set(NODE_ID, 30, TimeUnit.SECONDS);
log.debug("keep heart by redis,node id = [{}]",NODE_ID);
}, 1, 15, TimeUnit.SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(this::stopHeartbeat));
log.info("JVMProcessHeartbeat init successful");
} /**
* 停止心跳,关闭线程池
*/
private void stopHeartbeat() {
if (this.scheduledExecutorService != null) {
this.scheduledExecutorService.shutdown();
}
log.info("JVMProcessHeartbeat stop!");
} /**
* 检查指定的节点是否在线
*
* @param nodeId
* @return
*/
public boolean checkOnline(final String nodeId) {
String key = String.format(FORMAT, nodeId);
RBucket<String> bucket = redissonClient.getBucket(key);
if (bucket != null && bucket.isExists()) {
return true;
}
return false;
} }

支持主从切换的Bean

对于需要进行主从切换的bean,将其生命周期划分为以下几个阶段

  1. 初始化(init)
  2. turnMaster(切换为主)
  3. turnSlave(切换为从)
public interface MasterSlaveSwitchBean {

    /**
* 初始伦
*/
default void init() { } /**
* 切换为master为的触发事件
*/
default void turnMaster() { } /**
* 切换为slave后的触发事件
*/
default void turnSlave() { } /**
* 通过静态方法的方式将其进行注册
*
* @param applicationContext
* @param masterSlaveSwitchBean
*/
static void supportMasterSlave(ApplicationContext applicationContext, MasterSlaveSwitchBean masterSlaveSwitchBean) {
ProcessMasterSelector masterSelector = null;
MasterSlaveNamespace masterSlaveNamespace = masterSlaveSwitchBean.getClass().getAnnotation(MasterSlaveNamespace.class);
String namespace = Optional.ofNullable(masterSlaveNamespace).map(MasterSlaveNamespace::value).orElse(SystemConstant.DEFAULT_MASTER_SLAVE_NAMESPACE);
try {
masterSelector = applicationContext.getBean(namespace, ProcessMasterSelector.class);
} catch (Exception e) {
}
if (masterSelector == null) {
//如果是非集群状态下,则先init初始化,再运行turnMaster
masterSlaveSwitchBean.init();
masterSlaveSwitchBean.turnMaster();
} else {
//将其注册上去
masterSelector.register(new MasterSlaveSwitchBeanDecorator(masterSlaveSwitchBean));
}
}
}

为了更方便、更准确地控制支持主从切换bean的生命周期,为其添加一个包装类MasterSlaveSwitchBeanDecorator,重点关注其内部的boolean类型的属性 init 和 start

init方法在整个进程运行期间,只会被调用一次,而turnMaster 和 turnSlave则会根据切换可能被多次调用,这里有到了装饰者模式,实现如下 :

public class MasterSlaveSwitchBeanDecorator implements MasterSlaveSwitchBean {
/**
* 初始化标识
*/
@Getter
private boolean inited = false;
/**
* 是否已经运行
*/
@Getter
private boolean started = false; /**
* 具体的clusterBootstrapBean
*/
@Getter
private MasterSlaveSwitchBean masterSlaveSwitchBean; public MasterSlaveSwitchBeanDecorator(MasterSlaveSwitchBean masterSlaveSwitchBean) {
this.masterSlaveSwitchBean = masterSlaveSwitchBean;
}
@Override
public void init() {
long startTime = System.currentTimeMillis();
log.info("start handle [{}] init method", masterSlaveSwitchBean.getClass().getSimpleName());
masterSlaveSwitchBean.init();
inited = true;
log.info("end handle [{}] init method,cost time [{}] seconds", masterSlaveSwitchBean.getClass().getSimpleName(), (System.currentTimeMillis() - startTime) / 1000);
}
@Override
public void turnMaster() {
if (started) {
return;
}
log.info("start handle [{}] turn master method", masterSlaveSwitchBean.getClass().getSimpleName());
masterSlaveSwitchBean.turnMaster();
started = true;
}
@Override
public void turnSlave() {
if (!started) {
return;
}
log.info("start handle [{}] turn slave method", masterSlaveSwitchBean.getClass().getSimpleName());
masterSlaveSwitchBean.turnSlave();
started = false;
}
}

为了更方便地在代码中使用masterSlaveSwitchBean,为其添加一个抽象类,bean只需要继承这个抽象类,就能够被master selector动态地控制进行事件的触发。

实现者只需要继承该类即可

public abstract class AbstractMasterSlaveSwitchBean implements ApplicationContextAware, MasterSlaveSwitchBean {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
MasterSlaveSwitchBean.supportMasterSlave(applicationContext, this);
}
}

选主器(MasterSelector)

选主流程

  1. 系统启动后,定时地去维护一个选主的key,如果这个key的value值是自己的话,则自身就是master节点
  2. 当完成一次状态维护后,触发事件(根据当前注册上来的bean状态信息去执行具体的业务逻辑(三个方法 init turnMaster turnSlave))

    具体实现代码相对较多,已放至github,欢迎大家对不足之处进行指正。

    github地址:https://github.com/873098424/redisMasterSelctor.git

基于redis的选主功能设计的更多相关文章

  1. 基于redis实现的点赞功能设计思路详解

    点赞其实是一个很有意思的功能.基本的设计思路有大致两种, 一种自然是用mysql等 数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysq ...

  2. 简述 zookeeper 基于 Zab 协议实现选主及事务提交

    Zab 协议:zookeeper 基于 Paxos 协议的改进协议 zookeeper atomic broadcast 原子广播协议. zookeeper 基于 Zab 协议实现选主及事务提交. 一 ...

  3. [转载] 基于Redis实现分布式消息队列

    转载自http://www.linuxidc.com/Linux/2015-05/117661.htm 1.为什么需要消息队列?当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消 ...

  4. Etcd 使用场景:通过分布式锁思路实现自动选主

    分布式锁?选主? 分布式锁可以保证当有多台实例同时竞争一把锁时,只有一个人会成功,其他的都是失败.诸如共享资源修改.幂等.频控等场景都可以通过分布式锁来实现. 还有一种场景,也可以通过分布式锁来实现, ...

  5. 基于redis分布式缓存实现(新浪微博案例)

    第一:Redis 是什么? Redis是基于内存.可持久化的日志型.Key-Value数据库 高性能存储系统,并提供多种语言的API. 第二:出现背景 数据结构(Data Structure)需求越来 ...

  6. 基于Redis主从复制读写分离架构的Session共享

    1.搭建主从复制 第一步:将Redis拷贝到虚拟机上的指定文件夹内,此Redis作为主服务 第二步:将Redis拷贝到本机的指定文件夹内,此Redis作为从服务 第三步:修改主服务的配置文件(redi ...

  7. (转)基于Redis Sentinel的Redis集群(主从&Sharding)高可用方案

    转载自:http://warm-breeze.iteye.com/blog/2020413 本文主要介绍一种通过Jedis&Sentinel实现Redis集群高可用方案,该方案需要使用Jedi ...

  8. 基于Redis Sentinel的Redis集群(主从Sharding)高可用方案(转)

    本文主要介绍一种通过Jedis&Sentinel实现Redis集群高可用方案,该方案需要使用Jedis2.2.2及以上版本(强制),Redis2.8及以上版本(可选,Sentinel最早出现在 ...

  9. 基于Redis缓存的Session共享(附源码)

    基于Redis缓存的Session共享(附源码) 在上一篇文章中我们研究了Redis的安装及一些基本的缓存操作,今天我们就利用Redis缓存实现一个Session共享,基于.NET平台的Seesion ...

  10. 记一次企业级爬虫系统升级改造(六):基于Redis实现免费的IP代理池

    前言: 首先表示抱歉,春节后一直较忙,未及时更新该系列文章. 近期,由于监控的站源越来越多,就偶有站源做了反爬机制,造成我们的SupportYun系统小爬虫服务时常被封IP,不能进行数据采集. 这时候 ...

随机推荐

  1. 时序数据库永远的难关 — 时间线膨胀(高基数 Cardinality)问题的解决方案

    ​简介: 本文主要讨论 influxdb 在遇到写入的数据出现高基数 Cardinality 问题时,一些可行的解决方案. 作者 | 徐建伟 (竹影) 前序 随着移动端发展走向饱和,现在整个 IT 行 ...

  2. 浅谈 Linux 高负载的系统化分析

    ​简介: 浅谈 Linux 高负载的系统化分析,阿里云系统组工程师杨勇通过对线上各种问题的系统化分析. 讲解 Linux Load 高如何排查的话题属于老生常谈了,但多数文章只是聚焦了几个点,缺少整体 ...

  3. MYSQL深潜 - 剖析Performance Schema内存管理

    ​简介: 本文主要是通过对PFS引擎的内存管理的源码的阅读,解读PFS内存分配及释放原理,深入剖析其中存在的一些问题,以及一些改进思路.本文源代码分析基于Mysql-8.0.24版本. ​ 作者 | ...

  4. [Roblox] 从障碍跑入门构建基础平台游戏_罗布乐思

      对于障碍跑来说,底板部分是可以不需要的 这样掉下障碍物就结束游戏,Baseplate 可以在右侧资源管理器 选中后Delete. SpawnLocation 标记玩家在游戏开始时或重新开始后在世界 ...

  5. 非技术 对以后各大应用功能与 AI 助手的思考

    本文记录我对于 AI 助手在未来给各大应用或网站或设备带来的影响的思考 结论:未来的各大应用或网站或者是设备,都不会出现满屏的眼花缭乱的功能,取代的是各自有一个专属的 AI 助手,通过 AI 助手与人 ...

  6. 【爬虫+情感判定+Top10高频词+词云图】"乌克兰"油管热评python舆情分析

    目录 一.分析背景 二.整体思路 三.代码讲解 3.1 爬虫采集 3.2 情感判定 3.3 Top10高频词 3.4 词云图 四.得出结论 五.同步视频演示 六.附完整源码 一.分析背景 乌克兰局势这 ...

  7. 11.IO 流

    1.IO 流引入 概述:以应用程序为参照物,读取数据为输入流(Input),写数据为输出流(Output),大量输入输出数据简称 IO 流 原理: 2.IO 流的分类 读写的文件分类 二进制文件:打开 ...

  8. ansible(18)--ansible的selinux模块

    1. selinux模块 功能:管理远端主机的 SELINUX 防火墙: 主要参数如下: 参数 说明 state Selinux模式:enforcing.permissive.disabled pol ...

  9. C数据结构:树和森林存储方式与遍历方式

    文章目录 树的存储方式 双亲表示法 孩子链表表示法 孩子兄弟表示法(二叉树表示法) 树和二叉树的转换 森林和二叉树的转换 树和森林的遍历 树的遍历方式 森林的遍历方式 浅谈一下几个问题 为什么树没有中 ...

  10. C语言:顺序存储循环队列#保存文件中单词的最后三个字母

    目录 前言 全局变量和结构体代码 四个任务 ①计算文件中的单词数量 ②保存单词最后三个字母到链表中 ③将链表的内容导出到另一个文件中 ④将新文件的内容打印到屏幕中 @完整代码@ 前言 本博客最终会完成 ...