AOP 方案

AOP 方案

AOP是什么?

面向切面编程(Aspect Oriented Programming)一种编程思想
** OOP(面向对象编程)与AOP区别:**

  • OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分管理。
  • AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果
  • OOP负责抽象和管理,AOP负责解耦和复用
  • OOP面向名词领域,AOP面向动词领域
  • OOP面向纵向,AOP面向横向

为啥要使用AOP?

  • 降低业务耦合度;
  • 提高程序可复用性;
  • 提高代码可读性,易维护性;
  • 提高开发效率

实际上,基本上任何框架都需要提供类似的接口,因 为大家都需要解决一些共性问题,例如日志、追踪、 性能监控等。

开源实例

Beego ORM

image.png
image.png
可以说是没有。 Beego 在 ORM 层面上,类似的需求都 是通过侵入式的方案解决的,所以看不到一个显式的类 似于 Middleware ;
后来加一个 FilterChain,但看起来效果不是很好, 根源在于 ORM 没有一个统一的出口(即和数据库交互的 统一的出口)。 用户可以通过装饰器模式封装 Beego ORM 的接口来间 接实现类似的需求。

GORM

在 GORM 里面这个东西叫做 Hook,它是一个和时机有关的概念 :

  • Create:对应于插入 ;
  • Update: 对应更新 ;
  • Delete:对应删除 ;
  • Query:对应于查找 ;

所以用户需要根据自己的需求,选择不同的 Hook。 当然,其实 GORM 还提供了一些额外的接口,实现这些接口也能达成类似的效 果,比如说 driver ;

GORM Create Hook

image.png
image.png
Create 有四个,分成两对

  • BeforeSave 和 AfterSave
  • BeforeCreate 和 AfterCreate
GORM Update Hook

image.png
image.png
Update 也是四个 Hook,分成两对

  • BeforeSave 和 AfterSave
  • BeforeUpdate 和 AfterUpdate
GORM Delete Hook

image.png

image.png
Delete 有两个 Hook,它们构成了一对

  • BeforeDelete 和 AfterDelete
GORM Query Hook

image.png
image.png
Query 只有一个 Hook,就是 AfterFind

GORM 设计总结

优点

  • 分查询类型:对增删改查有不同的 Hook ;
  • 分时机:在查询执行前,或者在查询执行后。这种顺序是预定义好的 ;
  • 修改上下文:每一个 Hook 内部都是可以修改执行上下文的。例如可以利用 这个特性实现一个简单的分库分表中间件 ;
  • 用户用起来还是比较简单的,例如使用 AfterUpdate 的时 候,可以很清楚确定这个会在 Update 语句的时候被调用。

缺点也很明显

  • 缺乏扩展性,用户指定不了顺序
  • BeforeSave 和 AfterSave 有点令人困惑
  • 如果 GORM 要扩展支持别的接入点,例如 BeforeFind,需要修改

API 设计

image.png

  • 抽象出来一个 QueryContext,代表查询上下文
  • 抽象出来一个 QueryResult,代表查询结果
  • 抽象出来 Handler,代表在这个上下文里面做点什么事情
  • 抽象出来 Middleware,连接不同的 Handler
type QueryContext struct {
    Type string
    builder QueryBuilder
    Model   *model.Model
    q *Query
}

func (qc *QueryContext) Query() (*Query, error) {
    if qc.q != nil {
        return qc.q, nil
    }
    var err error
    qc.q, err = qc.builder.Build()
    return qc.q, err
}

type QueryResult struct {
    Result any
    Err error
}

type Middleware func(next HandleFunc) HandleFunc

type HandleFunc func(ctx context.Context, qc *QueryContext) *QueryResult

这种设计的缺陷就是用户实现 Middleware 的时候,可能存在大量的类型 断言之类的东西,或者需要自己判断是什么查询。

具体实现

查询日志

type MiddlewareBuilder struct {
	logFunc func(query string, args []any)
}

func NewMiddlewareBuilder() *MiddlewareBuilder {
	return &MiddlewareBuilder{
		logFunc: func(query string, args []any) {
			log.Printf("sql: %s, args: %v", query, args)
		},
	}
}

func (m *MiddlewareBuilder) LogFunc(fn func(query string, args []any)) *MiddlewareBuilder {
	m.logFunc = fn
	return m
}

func (m MiddlewareBuilder) Build() orm.Middleware {
	return func(next orm.HandleFunc) orm.HandleFunc {
		return func(ctx context.Context, qc *orm.QueryContext) *orm.QueryResult {
			q, err := qc.Builder.Build()
			if err != nil {
				return &orm.QueryResult{
					Err: err,
				}
			}
			m.logFunc(q.SQL, q.Args)
			res := next(ctx, qc)
			return res
		}
	}
}

现构造 SQL 都失败了,就可以直接返回 了。也可以选择继续执行下去,因为后面的 Middleware 可能还需要继续处理。
大多数的 ORM 框架都喜欢引入一个 DEBUG 的标记 位,这种 DEBUG 标记位的缺点是侵入式的方案,需要我们修改 Get、GetMulti 和 Exec 这几个方法。
相比之下,这种做法无侵入,用户的可控性更强。 另外,这里并没有处理敏感信息,也就是 Args 里面可能有密码之类的信息,logFunc 的提供者要 处理这种问题。
另外一种所谓的 dry run,其实也就是在这里记录了 SQL 之后就直接返回,根本不会发起真实调用。

opentelemetry

const defaultInstrumentationName = "middleware/opentelemetry"

type MiddlewareBuilder struct {
	Tracer trace.Tracer
}

func (b *MiddlewareBuilder) Build() orm.Middleware {
	if b.Tracer == nil {
		b.Tracer = otel.GetTracerProvider().Tracer(defaultInstrumentationName)
	}
	return func(next orm.HandleFunc) orm.HandleFunc {
		return func(ctx context.Context, qc *orm.QueryContext) *orm.QueryResult {
			tbl := qc.Meta.TableName
			reqCtx, span := b.Tracer.Start(ctx, qc.Type+"-"+tbl, trace.WithAttributes())
			defer span.End()
			span.SetAttributes(attribute.String("component", "orm"))
			q, err := qc.Builder.Build()
			if err != nil {
				span.RecordError(err)
			}
			span.SetAttributes(attribute.String("table", tbl))
			if q != nil {
				span.SetAttributes(attribute.String("sql", q.SQL))
			}
			return next(reqCtx, qc)
		}
	}
}

简单记录了一下表名和 SQL。 但是没有记录参数,如果要记录参数, 同样要处理加密的问题。

prometheus

type MiddlewareBuilder struct {
	Name        string
	Subsystem   string
	ConstLabels map[string]string
	Help        string
}

func (m *MiddlewareBuilder) Build() orm.Middleware {
	summaryVec := prometheus.NewSummaryVec(prometheus.SummaryOpts{
		Name:        m.Name,
		Subsystem:   m.Subsystem,
		ConstLabels: m.ConstLabels,
		Help:        m.Help,
	}, []string{"type", "table"})
	prometheus.MustRegister(summaryVec)
	return func(next orm.HandleFunc) orm.HandleFunc {
		return func(ctx context.Context, qc *orm.QueryContext) *orm.QueryResult {
			startTime := time.Now()
			defer func() {
				endTime := time.Now()
				summaryVec.WithLabelValues(qc.Type, qc.Meta.TableName).
					Observe(float64(endTime.Sub(startTime).Milliseconds()))
			}()
			return next(ctx, qc)
		}
	}
}

prometheus 也就是简单记录了一下操 作,以及对应的表。 在这几个 Middleware 里面可以看到,其 实没有办法拿到 IP 之类的信息。因为 ORM 层面并 不知道 Go sql 包内部的信息。 对于分库分表的数据库,这种监控过于弱了,因为在 分库分表之下,会希望能够单独监控每一个库。

单元测试

func Test_Middleware(t *testing.T) {
	testCases := []struct {
		name    string
		wantErr error
		mdls    []Middleware
	}{
		{
			name: "one middleware",
			mdls: func() []Middleware {
				var mdl Middleware = func(next HandleFunc) HandleFunc {
					return func(ctx context.Context, queryContext *QueryContext) *QueryResult {
						return &QueryResult{}
					}
				}
				return []Middleware{mdl}
			}(),
		},
		{
			name: "many middleware",
			mdls: func() []Middleware {
				mdl1 := func(next HandleFunc) HandleFunc {
					return func(ctx context.Context, queryContext *QueryContext) *QueryResult {
						return &QueryResult{Result: "mdl1"}
					}
				}
				mdl2 := func(next HandleFunc) HandleFunc {
					return func(ctx context.Context, queryContext *QueryContext) *QueryResult {
						return &QueryResult{Result: "mdl2"}
					}
				}
				return []Middleware{mdl1, mdl2}
			}(),
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			orm, err := Open("sqlite3", "file:test.db?cache=shared&mode=memory",
				DBWithMiddlewares(tc.mdls...))
			if err != nil {
				t.Error(err)
			}
			defer func() {
				_ = orm.Close()
			}()
			assert.EqualValues(t, tc.mdls, orm.ms)
		})
	}
}

func TestNewMiddlewareBuilder(t *testing.T) {
	var query string
	var args []any
	m := (&MiddlewareBuilder{}).LogFunc(func(q string, as []any) {
		query=q
		args =as
	})

	db, err := orm.Open("sqlite3",
		"file:test.db?cache=shared&mode=memory",
		orm.DBWithMiddlewares(m.Build()))
	require.NoError(t, err)
	_, _ = orm.NewSelector[TestModel](db).Where(orm.C("Id").EQ(10)).Get(context.Background())
	assert.Equal(t, "SELECT * FROM `test_model` WHERE `id` = ?;", query)
	assert.Equal(t, []any{10}, args)

	orm.NewInserter[TestModel](db).Values(&TestModel{Id: 18}).Exec(context.Background())
	assert.Equal(t, "INSERT INTO `test_model`(`id`,`first_name`,`age`,`last_name`) VALUES (?,?,?,?);", query)
	assert.Equal(t, []any{int64(18), "", int8(0), (*sql.NullString)(nil)}, args)
}

type TestModel struct {
	Id        int64
	FirstName string
	Age       int8
	LastName  *sql.NullString
}

总结

  • GORM 的 Hook 设计原理:GORM 的 Hook 按照 SQL 类型划分,例如 BeforeCreate 之类的。本质上 只是 GORM 的研发者在内部找准地方(其实就是指执行语句前后)调用用户注册的 Hook ;
  • 怎么监控慢查询?就是可以利用 AOP 方案,写一个 AOP 的实现,里面计算 SQL 执行时间,当 SQL 执 行时间超过阈值的时候就可以告警或者打印出来。但是所有 SQL 监控都要注意不要把敏感数据打印出来 ;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值