关于java:部分研发设计文档

1次阅读

共计 8243 个字符,预计需要花费 21 分钟才能阅读完成。

1. 角色权限模块

1.1 RBAC 概述

RBAC 通过定义角色的权限,并对用户授予某个角色从而来管制用户的权限,实现了用户和权限的逻辑拆散(区别于 ACL 模型),极大中央便了权限的治理

上面在解说之前,先介绍一些名词:

  • User(用户):每个用户都有惟一的 UID 辨认,并被授予不同的角色
  • Role(角色):不同角色具备不同的权限
  • Permission(权限):拜访权限
  • 用户 - 角色映射:用户和角色之间的映射关系
  • 角色 - 权限映射:角色和权限之间的映射

    1.2 以后零碎设计

权限零碎日益简单,需求方提出须要反对多种维度受权

如: 研发部的员工能够拜访 gitlab;java 开发工程师能够拜访跳板机; 杭州的员工能够看到亚运会信息;P6 级别以上能力看到公司利润报表。于是,零碎的受权也变得越来越简单,更有甚者,只有研发部部门的 leader 能够看到以后部门研发部成员的根本信息 …

多 tag 模型权限设计(tag 是反对受权的字段,维度也能够称之为 tag 标签)

因为通常是将某一类权限赋予给用户,故抽离出权限组的概念。权限组是若单个干权限的汇合

以后零碎:

查问权限的逻辑为

1. 依据 employeeId 查问 EmployeeRoleMap 表获取角色汇合 roleIds

select roleIds from EmployeeRoleMap where employeeId = ? 

2. 查问 permission 表获取权限关联:(以后 tag 只有 RoleDimssionKey.ROLE)

select menuUID,menuGroupId from permission where value in [roleIds...] and key = RoleDimssionKey.ROLE

3. 若存在 menuGroupId(权限组 id), 则查问 menu_group_mapping(权限 - 权限组关联表)获取权限组关联的所有 menuUID

select menuUID,menuGroupId from menu_group_mapping where menuGroupId in [...]

4. 依据 menuUID 查问所有 Menu(若步骤 3 中存在 menuId,累计一起查问)

select * from menu_group_mapping where menuId in [...]

权限组相干逻辑为

权限组配置(经营平台)

商品 spu 绑定有 menuGroup 属性(长期解决方案,前期倡议剥离商品属性,间接绑定对应的 spu 和权限组)

用户购买商品付款胜利后,后盾逻辑会查问出以后 sku 绑定的菜单组,并增加到 permission(tag- 权限关联表)中

insert into permission (KEY=ROLEDIMISSION.ROLEID,value=?,MENUGROUPID=?DATA_BI_MENU_GROUP_ID?)

2.sku 商品价格计算

为了避免薅羊毛,0 元价格商品只能购买一次

2.1 新用户 NoneUpgradeSkuFilter

间接查问 sku 商品价格即可

2.2 降级账号数量 UpgradeAccountSkuFilter

锁定 时长 = 离以后套餐最近的时长 , 账号数量大于以后套餐的账号 的套餐

2.3 降级时长 UpgradeTimeSkuFilter

锁定 账号数量等于以后套餐的账号 的套餐

代码逻辑为

1. 购买时查问 organization_payment_detail 表,确定能够购买的类型。(购买胜利会更新 organization_payment_detail)

organization_payment 为空(新用户)前端显示购买按钮,

organization_payment(过期或已购买状态)前端显示降级时长、降级账号按钮

2. 前端发动查问 sku 申请并携带购买类型参数,后端依据购买类型确定 filter 来进行商品的过滤和价格计算(如 NoneUpgradeSkuFilter、UpgradeAccountSkuFilter、UpgradeTimeSkuFilter)

由对应的购买类型如 UpgradeAccountSkuFilter 负责商品的过滤及价格的计算

计算逻辑为 补差价(理论价格 = 应酬价格 - 差价)

套餐 A 1 个月 10 个 10 元

套餐 B 1 个月 20 个 20 元

套餐 C 1 个月 30 个 30 元

套餐 D 4 个月 10 个 40 元

套餐 E 5 个月 10 个 50 元

1. 路人甲用户 降级账号
case1 假如明天是 09-15 日,09-01 日购买套餐 A

则可降级套餐为 B\C

如 购买 B 套餐价格为(20/30(30-15))-10/30×15= 5 元 能够简化为须要补 15 天的差价(30-15)x(20/30-10/30)= 5 元,以后(套餐变为 09-15—->09-30 日 20 个账号)

2. 路人甲用户 降级时长
case1 假如明天是 09-15 日,09-01 日购买套餐 A

则可降级套餐为 D\E

购买 D 价格为 40 元 -10 元 /30 天 * 未应用天数 15 天 =(40-10/30×15)=35 元,以后 套餐变为 09-15—->09-15 后 4 个月 10 个账号

购买 E 价格为 50 元 -10 元 /30 天 * 未应用天数 15 天 =(50-10/30×15)=45 元,以后 套餐变为 09-15—->09-15 后 5 个月 10 个账号

3. AD 模块

3.1 AD 域控根底

AD 是 windows 计算机近程登录的账户管理中心,关上近程利用,会为每个数影用户调配独立的办公空间即创立 AD 账号。AD 账号创立是通过 java 调用 powershell 命令行实现的

3.2 连接池

模仿 C3P0 连接池、线程池等原理实现一个能够复用的 powershell 连接池

需要剖析:以后零碎 powershell 次要用于帮助 DDC 机器、AD 相干资源 CRUD 及其他辅助 powershell 命令。因为 AD 域控是连通的,且 powershell 能够近程运行。故咱们冀望部署在 DDC01 机器上的 agent 能够间接管制本机和 app01\addc01 上 powershell 的经营

入参:机器名、script 脚本

Powershell:近程执行、本地执行

IRecycle 可复用对象。id 作为惟一标示,reset 办法重置所有属性

ObjectPool 形象可复用资源池,应用 LinkedBlockingQueue 作为容器,避免多线程并发平安问题

DefaultRecyclePowerShellFactory powershell 连接池
+String getId();
+void reset(); 销毁以后 powershell session 上下文
Remove-Variable * -ErrorAction SilentlyContinue  -Exclude @(...)

DefaultRecyclePowerShell powershell 可复用对象

4.websocket 模块

绝对于传统 HTTP 每次申请 - 应答都须要客户端与服务端建设连贯的模式,WebSocket 是相似 Socket 的 TCP 长连贯通信模式。WebSocket 连贯建设后,后续数据都以帧序列的模式传输。

握手阶段

a. 浏览器、服务器建设 TCP 连贯,三次握手。这是通信的根底,传输管制层,若失败后续都不执行。

b. TCP 连贯胜利后,浏览器通过 HTTP 协定向服务器传送 WebSocket 反对的版本号等信息。(开始前的 HTTP 握手)

c. 服务器收到客户端的握手申请后,同样采纳 HTTP 协定回馈数据。

d. 当收到了连贯胜利的音讯后,通过 TCP 通道进行传输通信。

客户端发送音讯:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13

服务端返回音讯:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

技术选型:

原生 websocket

springboot websocket(轻量级,spring 集成,开发成本小)

Stomp(相似于 spring stream 音讯,springboot websocket 高级协定,前端须要应用 SOCKJS)

Netty SocketIO(轻量级,性能好,前端须要引入 socket.io.js)

spring websocket 次要组件

WebSocketConfigurer websocket 配置类:增加音讯处理器和握手拦截器
void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
如 registry.addHandler(agentWSHandler(), "/api/v1/websocket/dsAgent")
                .setAllowedOrigins("*")
                .addInterceptors(agentWSInterceptor);  

TextWebSocketHandler 文本音讯处理器
void afterConnectionEstablished(WebSocketSession session)连贯建设胜利之后
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) 收到客户端推送的音讯
void afterConnectionClosed(WebSocketSession session, CloseStatus status) 连贯断开之前
  
HandshakeInterceptor 握手拦截器
beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) 握手之前。能够做音讯的拦挡逻辑解决

websocketSession 多实例存在以下问题:

A websocket 连贯 app1 服务器,下次申请负载平衡连贯到了 app02 服务器。这个时候服务端须要推送 websocket 音讯

解决方案:形象出 WebsocketSender 专门负责 message 的发送。以后实现为 RocketMqMessageSender

先查看以后服务是否存在符合条件的 websocketSession, 若存在间接发送,若不存在发送到 rocketMq 中期待其余实例拉取生产。(留神死循环问题,不要始终发)

4.1 DSClient-StoreFront

WebsocketConfiguration 配置类配置了两条 websocket 通道:Dsclient 侧、前端侧

Dsclient-storeFront
DsclientHandler  Dsclient 侧 websocket 音讯处理器 /api/v1/websocket/dsClient
ExtractParameterInterceptor 提取 request 中的参数并封装到 websocketSession 中
BinderIdCheckInterceptor 查看是否申请中具备 BinderId 参数

前端侧 -storeFront
WebClientHandler 前端侧 websocket 音讯处理器 /api/v1/websocket/webClient
AuthHttpSessionInterceptor 校验是否登录

WebClientHandler

INIT_BINDER_INFO 服务端返回 binderId 信息
REFRESH_APPLICATION_LIST  服务端转发 Dsagent 触发的 REFRESH_APPLICATION_LIST 工夫
OPEN_APPLICATION 关上利用,转发给 Dsagent
WEB_CLIENT_DIS_CONNECT 前端退出登录,转发给 Dsagent

DsClientHandler

PUSH_LATEST_APPLICATION_INFO 刚连贯时服务端会发送最新的本地利用列表
REPORT_LOCAL_APPLICATION_INFO 上报本地利用详情如装置进度,会触发 REFRESH_APPLICATION_LIST 事件

流程如下:

4.2 DSAgent-AgentManagerWeb

WSConfiguration websocket 配置类,配置 AgentWSHandler 和 AgentWSInterceptor
AgentWSHandler websocket 音讯处理器
AgentWSInterceptor 提取参数封装到 websocketSession 上下文中

Dsagent 侧 -storeFront

AgentWSInterceptor 负责 Dsagent 侧 websocket 握手。为了后续不再传递以后 session 的惟一标识信息,如 sessionId、machineSessionName 等,故在握手胜利时将这部分身份信息间接放入 websocketSession 中,相似 httpHeader 中的 cookie 标示
如
ws://localhost:9071/api/v1/websocket/dsAgent?machineName=machineName&machineSessionId=machineSessionId&userName=userName

AgentWSHandler 负责 Dsagent 音讯解决
MACHINE_SESSION_REPORT:Dsagent 上报会话利用信息
MACHINE_REPORT:Dsagent 上报 system0 机器信息
MACHINE_SESSION_LOGOUT: 经营平台下发。由服务端转发给 Dsagent

流程如下:

session 会话信息上报流程:(非 system0 用户)

1.DsAgent 每 15 秒全量上报以后 session 信息,即 MACHINE_SESSION_REPORT 事件

2. 服务端存储信息到 Redis,过期工夫为 20s

3. 经营平台前端查看会话治理,反对分页查问,含糊查问

4. 经营平台前端点击登记按钮,下发 MACHINE_SESSION_LOGOUT 给 DsAgent

5.Dsagent 收到 MACHINE_SESSION_LOGOUT,会话胜利登记。服务端 webs co ke t 断开清空 redis 中以后 session 会话信息

机器信息上报流程(system0 用户):DsAgent 每 15 秒上报机器信息(MACHINE_REPORT), 服务端存储音讯过期工夫为 20s

数据分页小工具:

redis 作为内存数据库 数据须要分页查问,依赖于 SimpleStringCache<T>。SimpleStringCache 会基于 @CacheIndex 注解构建索引 Map

例如:

    @Data
    @Accessors(chain = true)
    static class A{
        @CacheIndex
        private String name;
        @CacheIndex
        private String id;
    }

    public static void main(String[] args) {List<A> list = new ArrayList();
        A haha1 = new A().setName("haha").setId("51");
        A haha2 = new A().setName("shiha").setId("761");
        
        list.add(haha1);
        list.add(haha2);


        SimpleStringCache simpleStringCache = new SimpleStringCache(list);
        List<Map> filter = new ArrayList<>();
        Map map = new HashMap();
        map.put("id", "1");
        map.put("name", "sh");
        filter.add(map);

        simpleStringCache.query(filter).forEach(System.out::println);
    }

simpleStringCache 会构建如下索引 Map 用于疾速定位

Originate:<0,haha1><1,haha2>

IndexMap:

<id,51,[0]><id,761,[1]>

<name,haha,[0]>,<name,shiha,[1]>

{
  "id": [
    {
      "51": "0",
      "761": "1"
    }
  ],
  "name": [
    {
      "haha": "0",
      "shiha": "1"
    }
  ]
}

查问时会根据传入的 List<Map> filter 进行含糊查问

[

{“id”: “1”,

   "name",:"sh"

}

}]

如上述申请会命中 id 索引、name 索引,首先查问 IndexMap 依据 id= 1 含糊查问出【0,1】,依据 name=sh 含糊查问出【1】。and 关系故最终只命中【1】,最初后果去 originData 中查问最终 data 为 <1,haha2>

5. 拓展

5.1 散布式调度问题

目前我的项目中应用自定义 @DistributeTask 注解:通过分布式锁的形式简略躲避了高可用环境下任务调度的并发问题。APP01 执行调度时会应用 Redission 红锁创立一个分布式锁,工作执行完结后开释锁。APP02 工作来长期同样会获取这个分布式锁。

举荐 Xxx-job 解决分布式定时工作

5.2 外部服务鉴权问题

目前外部服务接口鉴权是依赖了公共模块 dsphere-rpc-auth

须要鉴权的外部服务如 dsphere-marketing-platform 须要依赖 dsphere-rpc-auth-service 模块,dsphere-rpc-auth-service 会通过 spring.factories 以 springboot starter 的形式注入一个 HandlerIntecptor, 该 HandlerIntecptor 会拦挡 url 合乎 /api/v1/auth/* 申请,确保申请头 header 中携带 AUTHORIZATION=xxx,否则校验失败。

5.3 remote debug

java 近程 debug 依赖

5.4 线上问题排查

arthas 反编译、动静批改并加载 clas 文件、jvm 调优及 gc 问题剖析

5.5 分布式自增序列 id

依赖于数据库 InnoDB 引擎行锁实现

@Component
@Slf4j
public class SequenceUtil {
    @Autowired
    private SequenceRepo sequenceRepo;

    /**
     * INNODB 引擎默认行锁,能够保障更改不产生失落(只存在以后一个原子性操作)* MVCC 机制 应用以后读 获取最新版本数据
     * @param sequenceEnum
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer getId(SequenceEnum sequenceEnum) {sequenceRepo.incrementCounter(sequenceEnum.getPrimaryKeyId());
        int counterByName = sequenceRepo.findCounterById(sequenceEnum.getPrimaryKeyId());
        log.info("id"+counterByName);
        return counterByName;
    }

}

public interface SequenceRepo extends CrudRepository<Sequence, Integer> {@Query(value = "update sequence set counter = counter + 1 where id = (:id)", nativeQuery = true)
    @Modifying
    @Transactional
    int incrementCounter(@Param("id")Integer id);

    @Query(value = "select counter from sequence where id = (:id)", nativeQuery = true)
    int findCounterById(@Param("id")Integer id);

}
正文完
 0