CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

什么是 syslog

syslog 是一种被广泛使用的用于日志记录的标准。许多设备(如打印机、路由器)以及跨平台的系统都使用 syslog 标准。它可以将生成日志的软件、存储日志的系统以及分析日志的应用分离开来。这使得来自不同类型系统的日志数据可以集中存储和分析。syslog 可以用于系统管理和安全审计,以及记录一般信息、分析和调试消息等。

syslog 由 Eric Allman 在 1980 年代开发,最初是作为 Sendmail 项目 的一部分。它很快被其他应用程序采用,并逐渐成为类 Unix 系统上的标准日志记录解决方案。在其他操作系统上也有多种实现,并且它常见于网络设备中,例如路由器。syslog 最初作为一种事实上的标准运行,没有发布任何权威的规范,因此存在许多实现,其中一些实现并不兼容。互联网工程任务组(IETF) 在 2001 年 8 月的 RFC 3164 中记录了当时的现状。随后,Syslog 在 2009 年 3 月通过 RFC 5424 被标准化。

syslog 采用客户端-服务器架构,其中 syslog 服务器监听并记录来自客户端的消息。Syslog消息可以通过多种协议传输,包括UDP、TCP和TLS。

  • syslog 发送方(Sender):生成日志消息的设备或应用程序。
  • syslog 接收方(Receiver):接收并存储日志消息的服务器。
  • syslog 协议:定义日志消息的格式和传输方式。

syslog 消息通常包含以下几个部分(不同的 RFC 标准略有不同):

  • 优先级(Priority):表示消息的严重程度和设施类型。
  • 时间戳(Timestamp):记录消息生成的时间。
  • 主机名(Hostname):生成消息的设备或应用程序的主机名。
  • 标签(Tag):标识生成消息的进程或应用程序。
  • 消息内容(Content):实际的日志信息。

syslog 的 RFC 文档:

实现 syslog 服务端

严格来说,本文并没有实现 syslog 服务端,只是 syslog 服务端的一个类库(gsyslog),该类库实现重点参考了 https://github.com/cnaude/go-syslog ,并使用了 gnet 网络库(https://github.com/panjf2000/gnet)重写。该类库主要实现了网络监听(支持 UDP 和 TCP 协议)和消息解析,并对外提供消息处理接口,实现自己的消息处理接口即可实现完整的 syslog 服务端。公司的 syslog 服务基于该类库已经完整实现消息处理接口、性能监测,并支持编码格式自动转化(虽然 syslog 是 UTF8 格式,但不少客户端可能发松的是 GBK 格式 )。

以下的代码只展示重点部分,具体源码,请自行下载。

网络监听

gnet 需要实现 OnBootOnTraffic,通过 OnTraffic来实现不同协议的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
type Server struct {
gnet.BuiltinEventEngine
eng gnet.Engine
addr string
network string

bufferSize int
workerPool *goroutine.Pool

codec codec.Codec
handler Handler
}

// NewServer returns a new Server
func NewServer() *Server {
return &Server{
handler: NewDefaultHandler(),
codec: AutomaticCodec,
workerPool: goroutine.Default(),
}
}

// SetHandler Sets the handler, this handler with receive every syslog entry
func (s *Server) SetHandler(handler Handler) {
s.handler = handler
}

func (s *Server) Boot() error {
err := gnet.Run(s, s.addr,
gnet.WithMulticore(true),
gnet.WithSocketRecvBuffer(s.bufferSize))
if err != nil {
return err
}

return nil
}

func (s *Server) Stop() error {
_ = s.eng.Stop(context.Background())
s.workerPool.Release()

return nil
}

func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
s.eng = eng

logging.Infof("syslog server is listening on %s\n", s.addr)

return gnet.None
}

func (s *Server) OnTraffic(conn gnet.Conn) (action gnet.Action) {
if s.network == "udp" {
return s.handleUdp(conn)
}

if s.network == "tcp" {
return s.handleTcp(conn)
}

if s.network == "unix" {
return s.handleUdp(conn)
}

return gnet.None
}

UDP

UDP 直接读可以缓存,而后通过 GetParser 获取到实际的解析器,之后调用解析器的 Parse进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *Server) handleUdp(conn gnet.Conn) (action gnet.Action) {
data, err := conn.Next(-1)
if err != nil {
logging.Errorf("syslog read buff, something wrong, error:%v", err)
return gnet.None
}

client := conn.RemoteAddr().String()
copyData := make([]byte, len(data))
copy(copyData, data)
_ = s.workerPool.Submit(func() {
p := s.codec.GetParser(copyData)
log, _ := p.Parse(copyData, client)
s.handler.Handle(log)
})

return gnet.None
}

TCP

TCP 通过编码器的 Decode 接口拆包,并获取实际的解析器,之后调用解析器的 Parse进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (s *Server) handleTcp(conn gnet.Conn) (action gnet.Action) {
for {
data, p, err := s.codec.Decode(conn)
if err != nil {
break
}

client := conn.RemoteAddr().String()
_ = s.workerPool.Submit(func() {
log, _ := p.Parse(data, client)
s.handler.Handle(log)
})

return gnet.None
}

if conn.InboundBuffered() > 0 {
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
logging.Errorf("failed to wake up the connection, %v", err)
return gnet.Close
}
}

return gnet.None
}

消息解析

编码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
type Codec interface {
// Decode 用于 TCP 拆包
Decode(conn gnet.Conn) ([]byte, parser.Parser, error)
// GetParser 获取解析器,用于格式解析
GetParser([]byte) parser.Parser
}

// rfc3164
type RFC3164Codec struct{}

func (f *RFC3164Codec) GetParser(data []byte) parser.Parser {
return rfc3164Parser
}

func (f *RFC3164Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
buf, _ := conn.Next(-1)

length := len(buf)
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(length)

return body, rfc3164Parser, nil
}

// rfc5424
type RFC5424Codec struct{}

func (f *RFC5424Codec) GetParser(data []byte) parser.Parser {
return rfc5424Parser
}

func (f *RFC5424Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
buf, _ := conn.Next(-1)

length := len(buf)
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(length)

return body, rfc5424Parser, nil
}


// rfc6587
type RFC6587Codec struct{}

func (f *RFC6587Codec) GetParser(data []byte) parser.Parser {
return rfc6587Parser
}

func (f *RFC6587Codec) Decode(conn gnet.Conn) ([]byte, parser.Parser, error) {
if conn.InboundBuffered() < 4 {
// 如果没有找到换行符,说明数据不完整,等待更多数据
return nil, rfc6587Parser, ErrIncompletePacket
}

lenBuf, _ := conn.Peek(4)
length := binary.BigEndian.Uint32(lenBuf)

_, _ = conn.Discard(4)

buf, _ := conn.Next(int(length))
body := make([]byte, length)
copy(body, buf)

_, _ = conn.Discard(int(length))

return nil, rfc6587Parser, nil
}

解析器

主要实现 rfc3164 和 rfc5424 的解析,解析的步骤比较繁琐,这里就不单独列出代码了,具体内容可以参看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Parser interface {
Parse(data []byte, client string) (*Log, error)
Location(*time.Location)
}

type Log struct {
// 结构化的数据
Header map[string]interface{} `json:"header"`
// 原始数据
Body []byte `json:"body"`

// 是否有错,有错时结构化数据不可能
Err error

// 辅助解析用,解析的过程中cursor会发生变化
cursor int
// 辅助解析用
len int
// 是否忽略标签
skipTag bool
}

处理接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Handler The handler receive every syslog entry at Handle method
type Handler interface {
Handle(log *parser.Log)
}

type DefaultHandler struct {
counter *int64
}

func NewDefaultHandler() *DefaultHandler {
return &DefaultHandler{
counter: new(int64),
}
}

// Handle entry receiver
func (h *DefaultHandler) Handle(log *parser.Log) {
atomic.AddInt64(h.counter, 1)

logging.Infof("number %d, header:%v, body:%v,", *h.counter, log.Header, log.GetString(parser.LogBody))
}

简易服务端

使用默认的 DefaultHandler 实现简易服务端

1
2
3
4
5
6
7
8
9
10
func Test_udp_server(t *testing.T) {
server := NewServer()
server.SetCodec(rfc3164Codec)
server.SetAddr("udp://0.0.0.0:514")
defer func(server *Server) {
_ = server.Stop()
}(server)

_ = server.Boot()
}

总结

Syslog 是一种强大的日志记录协议,适用于各种系统和应用程序的日志管理。通过实现一个简单的 syslog 服务端,可以更好地理解和掌握 syslog 的工作原理。本文介绍了syslog的基本概念、协议格式,实现 syslog 的服务端类库,并提供了简易服务端的实现。希望本文能为读者在日志管理和系统监控方面提供有价值的参考。

说明

在「使用国产化框架 Solon 的一些开发经验」中提到,我们只是在平台的一个应用开始使用 Solon 框架,并非一次性的完全替换。但随着 solon cloud gateway 官方版本发布,替换Spring Gateway 也成为可能,于是开始相关的替换工作。

我们的网关主要提供了统一授权和鉴权的功能,及最基础的服务路由能力。另外系统中个问卷功能是对外提供服务的,当时为了提供对外服务和减少Gateway的路由判断逻辑,增加一个api-gateway。

此次使用 solon gateway 进行替换需要完整的实现旧有Spring Gateway的功能,同时想利用 Solon 的本地 gateway 的路由分组功能合并api-gateway。

一样的,在开发前,需要完整阅读,Solon Gateway 的官网文档 (https://solon.noear.org/article/804)。一样的,文档说明简单,集成也不难。

Solon 版本:3.0.5

实现过程

引入依赖包

1
2
3
4
5
6
7
dependencies {
implementation platform(project(":ficus-parent"))

implementation("org.noear:solon-web")
implementation("org.noear:nacos2-solon-cloud-plugin")
implementation("org.noear:solon.cloud.gateway")
}

注意:为了简单起见,已经省去其他依赖;使用ficus-parent进行统一的版本管理,如果没有使用版本管理,添加对应依赖时需要设置版本号。

通用Web授权

管理端授权

提供用户密码授权和基于Token的切换租户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Api("管理端授权服务")
@Component(tag = "adminApi")
@Mapping("/auth")
public class AdminAuthController {
@Inject private AdminAuthService service;

@Post
@Mapping("login")
@ApiOperation("login")
public CommonResult<UserLoginRes> login(@Body LoginReq loginReq) {
return CommonResult.success(service.login(loginReq));
}

@Post
@Mapping("changeLoginOrg")
@ApiOperation("changeLoginOrg")
public CommonResult<UserLoginRes> changeLoginOrg(@Body ChangeOrgReq changeOrgReq) {
return null;
}
}

应用端授权

提供基于域名的租户识别,用户密码授权及手机授权和用户注册等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Api("应用端授权服务")
@Component(tag = "appApi")
@Mapping("/auth")
public class AppAuthController {
@Inject private AppAuthService authService;

@Post
@Mapping("/getOrgByDomain")
@ApiOperation("getOrgByDomain")
public CommonResult<SimpleOrgRes> getOrgByDomain(@Body GetOrgReq getOrgReq) {
return CommonResult.success(authService.getOrgByDomain(getOrgReq));
}

@Post
@Mapping("/login")
@ApiOperation("login")
public CommonResult<MemberLoginRes> login(Context context, @Body LoginReq loginReq) {
Ret ret = checkLoginReq(loginReq);
if (ret.isFail()) {
ErrorCode error = ret.getAs("error");
return CommonResult.error(error);
}

return authService.login(context, loginReq);
}

@Post
@Mapping("/smsLogin")
@ApiOperation("smsLogin")
public CommonResult<MemberLoginRes> smsLogin(Context context, @Body SmsLoginReq smsLoginReq) {
Ret ret = checkSmsLoginReq(smsLoginReq);
if (ret.isFail()) {
ErrorCode error = ret.getAs("error");
return CommonResult.error(error);
}

return authService.smsLogin(context, smsLoginReq);
}

@Post
@Mapping("/register")
@ApiOperation("register")
public CommonResult<MemberLoginRes> register(Context context, @Body RegisterReq registerReq) {
return authService.register(context, registerReq);
}

@Post
@Mapping("/sendSms")
@ApiOperation("sendSms")
public CommonResult<Boolean> sendSms(Context context, @Body SendSmsReq sendSmsReq) {
return authService.sendSms(context, sendSmsReq);
}
}

路由分组

  • 管理端路由分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author airhead
*/
@Mapping("/**")
@Component
public class AdminGateway extends Gateway {
@Override
protected void register() {
// admin的异常抛出,可能包含SQL,起码更详细
filter(-1, new AdminExceptionFilter());
filter(0, new TenantFilter());

addBeans(bw -> "adminApi".equals(bw.tag()));
}
}
  • 应用端路由分组
1
2
3
4
5
6
7
8
9
10
11
12
@Mapping("/app-api/**")
@Component
public class AppGateway extends Gateway {
@Override
protected void register() {
// app的异常抛出不应该暴露SQL等
filter(-1, new AppExceptionFilter());
filter(0, new TenantFilter());

addBeans(bw -> "appApi".equals(bw.tag()));
}
}

响应式路由

实现

通过RouteFilterFactory定制路由过滤规则,实现不同类型的接口过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Component
@Slf4j
public class AuthFilterFactory implements RouteFilterFactory {
public static final String ADMIN = "admin";
public static final String APP = "app";
public static final String OPEN = "open";

@Inject private JwtConfig jwtConfig;
@Inject private AdminAuthConfig adminAuthConfig;
@Inject private AdminAuthService adminAuthService;

@Inject private AppAuthConfig appAuthConfig;
@Inject private AppAuthService appAuthService;

@Inject private OpenAuthConfig openAuthConfig;
@Inject private OpenAuthService openAuthService;

public static Completable error(ExContext ctx, ErrorCode errorCode) {
String resultStr = ONode.stringify(CommonResult.error(errorCode));
ctx.newResponse().header("Content-Type", "application/json;charset=UTF-8");
ctx.newResponse().body(Buffer.buffer(resultStr));
if (errorCode.getCode() < HTTP_STATUS_MAX) {
// http status的正常范围的错误
ctx.newResponse().status(errorCode.getCode());
} else {
ctx.newResponse().status(HTTP_INTERNAL_SERVER_ERROR);
}

return Completable.complete();
}

@Override
public String prefix() {
return "Auth";
}

@Override
public ExFilter create(String config) {
if (ADMIN.equals(config)) {
return new AdminAuthFilter(jwtConfig, adminAuthConfig, adminAuthService);
} else if (APP.equals(config)) {
return new AppAuthFilter(jwtConfig, appAuthConfig, appAuthService);
} else if (OPEN.equals(config)) {
return new OpenAuthFilter(jwtConfig, openAuthConfig, openAuthService);
} else {
return new EmptyAuthFilter();
}
}

/** 默认的空鉴权 */
public static class EmptyAuthFilter implements ExFilter {
public EmptyAuthFilter() {}

@Override
public Completable doFilter(ExContext ctx, ExFilterChain chain) {
log.info("empty filter");
return error(
ctx,
ErrorCode.builder()
.code(HTTP_NOT_FOUND)
.error(NOT_FOUND_ERROR)
.msg(NOT_FOUND_ERROR)
.build());
}
}
}

注意:这里AdminAuthFilter、AppAuthFilter和OpenAuthFilter未提供实现,可参考EmptyAuthFilter实现具体业务逻辑,或者参考官网例子。

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
solon.cloud:
nacos:
server: ${NACOS_SERVER:127.0.0.1:8848}
gateway:
httpClient:
responseTimeout: 180 #单位:秒
routes:
# app-api 用index=-1
- id: topcloud-oa-api
target: lb://topcloud-oa
index: -1
predicates:
- Path=/OA/app-api/**
filters:
- Auth=app

# open-api 用index=-1
- id: topcloud-uac-open
target: lb://topcloud-uac
index: -1
predicates:
- Path=/UAC/open-api/**
filters:
- Auth=open

# admin-api 用index=0
- id: topcloud-uac-admin
target: lb://topcloud-uac
index: 0
predicates:
- Path=/UAC/**
filters:
- Auth=admin

注意:3.0.5 版本如果路由存在包含关系,必须指定index,越具体的地址优先级要越高优先级(也就是index越小)。

可能碰到的问题

端口冲突

如果使用较低版本的 solon 时,提示端口冲突时,注意排除 solon-boot-xxx。

1
2
3
  implementation("org.noear:solon-web") {
exclude group: "org.noear", module: "solon-boot-smarthttp"
}

路由匹配未按预期

3.0.5 版本如果路由存在包含关系,必须指定index,越具体的地址优先级要越高优先级(也就是index越小)。

配置多个 predicates 的 Path导致路由无法匹配

3.0.5 版本不支持配置多个路由的匹配,3.0.6版本将支持,需要匹配多个Path时,配置多个路由。

效果

新版的 Solon Gateway 已经在开发线稳定运行一个多月,另外内存使用方面比Spring Gateway 少了五百兆左右。

  • Spring-gateway

  • Solon-gateway

「定投搜索」更新内容:

  • 增加首页

    • 提供基于标题的模糊搜索,返回匹配的 3 条课程记录。

    • 最新课程,显示最近整理的课程,可以直观看到课程的同步情况。

  • 课程内容

    • 使用 Qwen 大模型校对课程内容。
    • 人工校对课程内容。
  • 服务重构

    • 「定投搜索」和「类比宝库」的机器人合并成一个服务进行。
    • 优化校对和课程索引流程。

特别感谢小熊老师帮忙校对课程内容。

如果发现工具的问题,可以添加我的微信「Crazy_Airhead」,或 Mixin 号「1091586」,进行反馈,谢谢。

说明

本文主要介绍使用 Gradle 工具发布 Jar 包到中央仓库的方法及可能碰到的问题的处理。本文假定读者已经熟悉使用 Gradle,IDEA 等工具,并对中央仓库有所了解。本文使用 macOS,在安装gpg工具时可能有所不同,如果使用 Windows 系统需要自行查阅相关安装方法。

阅读全文 »

问题

系统升级 macOS Sequoia 的 15.1 后,今天要安装安装 gpg 结果提示异常:_error: gnupg: unknown or unsupported macos version: :dunno

分析

macOS 系统版本的升级可能导致 Homebrew 识别新系统版本时出现兼容性问题,具体表现为“unknown or unsupported macOS version: :dunno”错误。通过查看 Github 的讨论(来源),发现该问题可以通过重置 Homebrew 解决。

处理

执行brew docker,检查brew是否有问题。我自己执行此命令提示如下问题:

1
2
3
4
5
6
7
8
9
10
11
Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: Suspicious https://github.com/Homebrew/brew git origin remote found.
The current git origin is:
https://mirrors.aliyun.com/homebrew/brew.git

With a non-standard origin, Homebrew won't update properly.
You can solve this by setting the origin remote:
git -C "/opt/homebrew" remote set-url origin https://github.com/Homebrew/brew

此时继续执行提示的命令,因为墙的原因,之前设置了国内的镜像。

1
git -C "/opt/homebrew" remote set-url origin https://github.com/Homebrew/brew

再次执行brew docker,提示正常。

执行brew update-reset,将 Homebrew 重置到官方的稳定版本:

1
brew update-reset

接着就可以执行brew update,更新 Homebrew 及所有组件到最新版本。

1
brew update

参考

macOS系统升级Homebrew报错“unknown or unsupported macOS version: :dunno (MacOSVersionError) ”解决方法

一开始发现飞书的菜单栏里显示的推送消息是乱码的,而且在飞书界面中才侧边栏,显示的的已打开的标题也是乱码。一开始以为是飞书的问题,记得是前几天前有升级过,简单找了下飞书的语言相关的设置,没有找到可以配置的地方。于是就将就着用着,影响也不大,虽然标题乱码了,但内容显示是正常的。昨天 Mixin Messenger 推送了新的桌面版本,也做了升级。打开后发现会话列表都是乱码的。突然就想这个应该不是单个软件的问题了,应该是操作系统的配置。

我升级过 MacOS 的最新推送,也就是 macOS Sequoia 的 15.1 版本。首先想到的就是看看设置里的「地区与语言」,我个人的习惯是首选语言是英语,地区是大陆,多少会有点关系。既然中文乱码,那我就先把首选语言修改成中文,然后重启,飞书和 Mixin 的乱码问题都消失了,更加确定就是MacOS升级后引起的配置的问题。于是带着碰碰运气的方式,将首选语言设置成英语,重启后,还是正常显示。于是困扰了几天的问题处理好了。

因为这个问题是一次性的问题,也没有截图。当时想如果没解决,是想具体看看 mac 的系统编码的,现在的系统编码如下:

1
2
3
4
5
6
7
8
9
10
% locale

LANG="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_CTYPE="UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_ALL=

总之,如果你和我一样使用的是英语作为首选项,恰好也碰上部分软件突然就显示乱码问题,可以尝试调整下首选语言。

说明

在“「定投搜索」工具的一些分享”中有说到,通过 whsiper.cpp 识别的文本不准确的问题,包括存在繁体中文,错误的同音字,没有标点符号,突然增加的一些版权信息和告别语。

已经尝试使用 GLM4 进行文本的修改,修改效果不错。

以下分享此次使用 GLM4-9B 修改语音识别文本错误的过程。

阅读全文 »

说明

「定投搜索」工具是 Mixin 的机器人(Mixin 号「7000103414」),以 Mixin 聊天的方式提供「定投人生」课堂的全文搜索,并返回匹配的课程列表或者课程卡片。

最近学习AI,发现可以用AI技术对「定投搜索」进行些优化改造,然后在查看文档时发现之前写了这篇技术总结,现在稍作调整分享这个「定投搜索」Mixin的机器人制作过程,顺便预告下新版功能。

阅读全文 »

背景

公司的数字安全运营平台是基于 Spring Boot 和 Spring Cloud 进行构建的。考虑到公司有企事业单位的客户,陆续对产品提国产信创的要求。虽然当前信创可能只要求硬件,操作系统和数据库是国产化即可,但底层框架也是国产化的,可以给客户提供更多的国产化保证。

Solon 就是这样的一个选择,Solon 的官网介绍说,Solon 是 Java 「新式」应用开发框架,从零开始构建,有自主的标准规范与开放生态。追求更快、更小、更简单; 提倡克制、简洁、高效、开放、生态;是信通院可信开源社区成员、可信开源项目。Solon 有以下的特色:拥有更高的计算性价比,并发高 2~3 倍,内存省 50%;更快的开发效率:内核小,入门快,调试重启可快至 10 倍;更好的生成和部署体验:打包最多缩小 90%; 更大的兼容范围:非 java-ee 框架,同时支持 java8~java22,graalvm native image。

阅读全文 »

背景

上一篇「基于Mybatis的多租户实现方法」介绍的是Spring Boot使用 Mybatis Plus实现Db层的数据隔离。但在实际的实现过程中,除了涉及Db层的数据隔离,还会涉及租户的识别,缓存的数据隔离,ElasticSearch的数据隔离,任务的数据隔离、消息队列的隔离,同时也包括租户的切换与忽略租户等。

因此在上一篇的基础上,这次尝试把多租户的技术实现写得更完整些,如有不足支持欢迎指正。

阅读全文 »