服务端集群搭建
当单个服务端节点无法满足当前业务吞吐量或者业务要求时,服务端将会采用集群方式对外提供服务。URule属于Web应用,所以URule服务端的集群搭建也和传统的Web应用一致,有一个负载+n个服务端节点。
接下来我们将提供一种比较简便的Redis session共享的方式搭建,并借助nginx实现负载均衡。
1. 服务端集群

2.在集群环境上实现Session共享
关于session共享的方式有多种,常见的有:
(1)通过nginx的ip_hash,根据ip将请求分配到对应的服务器,相当于开启了会话保持功能
(2)基于关系型数据库存储
(3)基于cookie存储
(4)服务器内置的session复制域
(5)基于nosql(memcache、redis都可以)
实现原理也比较简单,在所有的请求之前配置一过滤器,在请求之前操作session,其实spring-session中真正起作用的session过滤器是SessionRepositoryFilter。而spring-session集成了redis与mongodb,若启用redis方式只需要稍加配置即可实现。
2.1 集成Redis实现Session共享
(1)集成spring-session-data-redis
<!-- 引入redis的集成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 引入 session与redis的集成:若不用redis请去掉本依赖 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
(2)配置类添加(非必须)
@Configuration
//不设置默认30分钟
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class RedisSessionConfig {
@Bean("redisTemplate")
@ConfigurationProperties(prefix="spring.redis")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//将key的序列化设置成StringRedisSerializer
StringRedisSerializer keySerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@EnableRedisHttpSession:开启Session共享功能。使用此注解之后Session调用会自动通过Redis存储和获取。另外,想要达到Session共享的目的,在其他的系统上只需要做同样的配置即可。 其中maxInactiveIntervalInSeconds参数是设置Session失效时间,在使用Redis Session之后,原Spring Boot的server.session.timeout属性不再生效。
(3)修改Session的储存方式为redis
# session的存储方式配置为redis
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
database: 1
timeout: 10000
jedis:
pool:
max-active: 50 # 连接池最大连接数(使用负值表示没有限制)
max-wait: 5000 # 连接池中连接用完时,新的请求等待时间(毫秒),超过该时间抛出异常JedisConnectionException,(默认-1,负值表示没有限制,不建议使用默认值)
max-idle: 10 # 连接池中的最大空闲连接,默认8
min-idle: 2 # 连接池中的最小空闲连接,默认0
2.2 nginx配置负载均衡示例
nginx的配置文件如下
upstream nginx-cluster{
server localhost:8080;
server localhost:8081;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://nginx-cluster;
}
}
2.3 测试页面
访问nginx地址http://192.168.1.16:80/,会自动跳由到任一tomcat实例上,通过日志可以发现上述nginx配置的分发策略默认是轮询

2.4 实现规则设计器中复制粘贴的多实例同步
@Service
public class SessionRedisClipboardStore implements ClipboardStore {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value) {
String sessionkey = RequestHolder.getRequest().getSession().getId()+key;
stringRedisTemplate.opsForValue().set(sessionkey,value);
RequestHolder.getRequest().getSession().setAttribute(sessionkey, value);
}
@Override
public String get(String key) {
String sessionkey = RequestHolder.getRequest().getSession().getId()+key;
String val = stringRedisTemplate.opsForValue().get(sessionkey);
if(StringUtils.hasText(val)){
return val;
}
return (String) RequestHolder.getRequest().getSession().getAttribute(sessionkey);
}
@Override
public void remove(String key) {
String sessionkey = RequestHolder.getRequest().getSession().getId()+key;
stringRedisTemplate.delete(sessionkey);
RequestHolder.getRequest().getSession().removeAttribute(sessionkey);
}
}
3.知识包数据同步问题
重点说明:
- 知识包在发布后被编译成一个rete树,RETE算法的核心是以‘内存空间换取执行时间’,故而发布后的知识包是直接存储在jvm虚拟内存中,那么要解决的是各实例内存中知识包数据同步更新问题
- 解决这个问题参考文档14.6.知识包多环境部署 · GitBook
- 我们也可以自行实现PacketPublishListener接口,用来拦截发布知识包操作,将状态写入到MQ中来通知其它实例自行加载最新的知识包数据
- 另外,知识包数量越多(体积越大),占用的jvm内存空间也越大,那么我们在生产环境上设置jvm堆内存大小建议在4g以上
而事实上‘知识包集群环境下内存同步问题与上述session共享并无关联’,session是由于多实例造成缓存不同步问题,而知识包是内存不同步造成的,内存比缓存性能更高。
3.1 多服务端集群同步
在集群环境下,由于服务端会有多个实例节点,各实例(jvm)之间是物理隔离的,那么当在某一个实例上【发布/启用知识包新版本】时,就需要立即同步到其它服务实例的jvm内存中,我们可以在系统管理端中维护集群实例信息来解决这一问题。
更多配置请查看文档3.8.集群管理 · GitBook,,都是在集群环境下内存同步引起的一系列问题。
- 集群(8080,8081实例节点维护)

- 知识包数据同步更新
在知识包页面,【查看当前已发布的知识包】点击启用后,利用http rpc通知各实例从数据库加载最新的知识包数据到jvm内存中,会弹出窗口(若事先没有配置集群节点数据,那就不会弹出如下窗口)自动显示同步结果,如下图所示:

在上述2个tomcat的控制台可以看到日志 Successfully reload file packet package:310
3.2 多客户端集群同步
(1)主动推送到客户端
- 客户端地址维护

- 查看当前已发布的知识包,在点击【推送到客户端】,如下操作所示

- 点击上图“确定”按钮后,弹出如下图所示界面(若没有配置在客户端地址,是不会显示如下列表)

在客户端tomcat控制台能看到以下日志:
Successfully receive the server side to pushed package:310(fc399b91f72240d89bc6d58fdd50538f)(1.0.3)
(2)被动拉取到客户端
- 在客户端(消费方)通过sdk调用知识包时,会从指定的服务器获取最新的知识包,实际上取决于urule.knowledgeUpdateCycle=?的值,即同步策略。

- 用postman调用客户端服务

- 查看控制台,发现了每次运行知识包时都会从服务端加载最新的

(3)客户端启动对账(active 版本校验)
客户端(消费方)在调用知识包触发加载 / 刷新(ClientCacheManager.reloadKnowledge)时,会与服务端进行启动对账:
- 通过
RemoteService.getKnowledge(packetId, version, timeStamp)向服务端拉取当前激活(active)版本的知识包; - 拿到服务端返回包后调用
KnowledgePackageImpl.initForActiveVersion(),基于服务端当前激活版本重新构建 Rete 网络; - 比较本地已缓存版本与服务端 active 版本,若两者不一致,会输出对账日志并刷新为服务端 active 版本,确保客户端持有的知识包与服务端启用的版本保持一致,避免客户端长期持有过期版本:
Reconcile knowledge package version mismatch, packetId=xxx, localVersion=1.0.2, serverActiveVersion=1.0.3, refresh to server active version.
该对账逻辑仅在版本号不同时触发刷新,不影响正常同步策略(urule.knowledgeUpdateCycle)。
3.3 自定义同步方式
重写PacketPublishListener.java接口,再结合消息中间件MQ来实现消息的发布和订阅等。
todo
4. 知识包同步日志(URULE_SYNC_LOG)
在集群与客户端同步过程中(启用 / 禁用知识包、变更版本、推送到客户端、服务端集群刷新、删除知识包等),系统会把每次同步操作的结果记录到数据库表 URULE_SYNC_LOG,用于事后审计与排错。该表由 SyncLogUtils 统一写入,写入路径同时覆盖成功与失败(失败时额外进入 SyncFailureStore 供重试)。
4.1 建表与多数据库支持
URULE_SYNC_LOG 的建表语句已内置在控制台提供的全部数据库 schema 脚本中,开箱即用,无需手动创建:
urule-schema-mysql.sql/urule-schema-oracle.sql/urule-schema-postgresql.sqlurule-schema-db2.sql/urule-schema-hsql.sql/urule-schema-informix.sql/urule-schema-sqlserver.sql/urule-schema-sybase.sql
各库脚本的字段、顺序、注释保持一致,仅类型与时间函数按方言适配(例如 mysql 用 CURRENT_TIMESTAMP,oracle 用 sysdate,postgresql 用 now())。初始化数据(含序列 URULE_SYNC_LOG.dbid)位于 urule-data.sql(sqlserver 另见 urule-data-sqlserver.sql)。
4.2 表字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| ID_ | 主键 | 自增 / 序列主键 |
| GROUPID | VARCHAR(50) | 团队编号 |
| PACKETID | 数值 | 知识包 ID |
| VERSION_ | VARCHAR(50) | 知识包版本 |
| TARGETTYPE | VARCHAR(20) | 同步目标类型:client(客户端) / cluster(服务端集群) |
| TARGETNAME | VARCHAR(255) | 同步目标名称 |
| TARGETURL | VARCHAR(500) | 同步目标地址 |
| EVENTTYPE | VARCHAR(20) | 事件类型(小写,见 4.3) |
| SUCCESS_ | BIT(1) / BOOLEAN | 是否同步成功 |
| ERRORMSG | 长文本 | 失败原因(成功时为空) |
| USERID | VARCHAR(50) | 操作人 ID |
| USERNAME | VARCHAR(100) | 操作人用户名 |
| CREATEDATE | 时间 | 创建时间(默认当前时间) |
4.3 事件类型(EVENTTYPE)
EVENT_TYPE_ 入库值统一为小写,取值如下:
| 枚举 | EVENTTYPE(小写) | 触发场景 |
|---|---|---|
ClientEvent.PUSH |
push |
主动推送到客户端 |
ClientEvent.ENABLE |
enable |
启用知识包(通知客户端加载) |
ClientEvent.DISABLE |
disable |
禁用知识包(通知客户端卸载) |
ClientEvent.CHANGE_VERSION |
change_version |
知识包变更版本并通知客户端 |
ClientEvent.CLUSTER_REFRESH |
refresh |
服务端集群节点刷新 / 重置缓存 |
ClientEvent.REMOVE |
remove |
删除知识包 / 移除项目 |
说明:调用方在代码中传入的可能是大写枚举名(如
PUSH),SyncLogUtils在入库前会统一toLowerCase()兜底,因此无论上游传大写还是小写,URULE_SYNC_LOG中落库的EVENT_TYPE_始终为小写值。
4.4 失败重试
当某次同步失败时,除在 URULE_SYNC_LOG 记录 SUCCESS_=0 外,还会写入 SyncFailureStore(URULE_PROPERTY 中的 urule.sync.failure 前缀)。服务端在集群节点列表变更或知识包变更时会自动触发重试;重试匹配 EVENT_TYPE_ 时忽略大小写,与 4.3 的小写约定兼容。