ecodeclub / eorm

简单 ORM 框架
Apache License 2.0
194 stars 64 forks source link

分库分表hash、shadow_hash算法不符合预期 #173

Closed heroyf closed 1 year ago

heroyf commented 1 year ago

问题简要描述

分库分表hash、shadow_hash算法不符合预期

复现步骤

复现环境: 仅做分表处理,存在3张表 ordertab{0,1,2},分表算法为UserId%3,UserId为分片键

针对问题1,设计如下单测用例,发现不通过:

{
    name: "select eq broadcast",
    builder: func() sharding.QueryBuilder {
        s := NewShardingSelector[Order](shardingDB).Where(C("OrderId").EQ(123))
        return s
    }(),
    qs: []*sharding.Query{
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_0` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_1` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_2` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
    },
}

上述单测用例,在执行时,输入为select * fromorder_db.order_tablewhere OrderId = '123'; 预期输出为3条查询语句 实际输出为空

针对问题2,原有单测用例where or-and all 不符合预期

{
    name: "where or-and all",
    builder: func() sharding.QueryBuilder {
        s := NewShardingSelector[Order](shardingDB).
            Select(C("OrderId"), C("Content")).
            Where(C("UserId").EQ(123).Or(C("UserId").EQ(181).And(C("UserId").EQ(234))))
        return s
    }(),
    qs: []*sharding.Query{
        {
            SQL:        "SELECT `order_id`,`content` FROM `order_db`.`order_tab_0` WHERE (`user_id`=?) OR ((`user_id`=?) AND (`user_id`=?));",
            Args:       []any{123, 181, 234},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `order_id`,`content` FROM `order_db`.`order_tab_1` WHERE (`user_id`=?) OR ((`user_id`=?) AND (`user_id`=?));",
            Args:       []any{123, 181, 234},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `order_id`,`content` FROM `order_db`.`order_tab_2` WHERE (`user_id`=?) OR ((`user_id`=?) AND (`user_id`=?));",
            Args:       []any{123, 181, 234},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
    },
},

上述单测用例,在执行时,输入为SELECTorder_id,contentFROMorder_db.order_tab_0WHERE (user_id=123) OR ((user_id=181) AND (user_id=234)); 理论预期为1条查询语句即

{
    SQL:        "SELECT `order_id`,`content` FROM `order_db`.`order_tab_0` WHERE (`user_id`=?) OR ((`user_id`=?) AND (`user_id`=?));",
    Args:       []any{123, 181, 234},
    DB:         "order_db",
    Datasource: "0.db.cluster.company.com:3306",
}

实际输出与单测用例预期输出均为3条

错误日志或者截图

image

针对问题1,可以看到输出的sharding.Query为空,与预期输出不符

image

针对问题2,实际单测执行通过,但是与预期不太符合

你期望的结果

执行如下单测能正常通过

{
    name: "select eq broadcast",
    builder: func() sharding.QueryBuilder {
        s := NewShardingSelector[Order](shardingDB).Where(C("OrderId").EQ(123))
        return s
    }(),
    qs: []*sharding.Query{
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_0` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_1` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
        {
            SQL:        "SELECT `user_id`,`order_id`,`content`,`account` FROM `order_db`.`order_tab_2` WHERE `order_id`=?;",
            Args:       []any{123},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
    },
},
{
    name: "where or-and all",
    builder: func() sharding.QueryBuilder {
        s := NewShardingSelector[Order](shardingDB).
            Select(C("OrderId"), C("Content")).
            Where(C("UserId").EQ(123).Or(C("UserId").EQ(181).And(C("UserId").EQ(234))))
        return s
    }(),
    qs: []*sharding.Query{
        {
            SQL:        "SELECT `order_id`,`content` FROM `order_db`.`order_tab_0` WHERE (`user_id`=?) OR ((`user_id`=?) AND (`user_id`=?));",
            Args:       []any{123, 181, 234},
            DB:         "order_db",
            Datasource: "0.db.cluster.company.com:3306",
        },
    },
}

你排查的结果,或者你觉得可行的修复方案

image

实际的分片实现逻辑不对,当传入的查询语句没有找到分片键时,应该做广播处理 理论上应该返回 return sharding.Result{Dsts: h.Broadcast(ctx)}, nil

当然这里修改后,此方法也需要做对应逻辑的调整 func (s *ShardingSelector[T]) findDstByPredicate(ctx context.Context, pre Predicate) (sharding.Result, error)

你使用的是 eorm 哪个版本?

eorm dev分支

flycash commented 1 year ago

确实,我们需要修复一下这个问题。可以单独作为一个合并请求

Stone-afk commented 1 year ago

这个问题是在实现增强的 ShardingSelector 留下的,主要考虑到的是如果直接在Sharding里执行广播,那 会直接影响findDstByPredicate里 or 分支的逻辑,考虑到这点的话当时是想把这个问题留到具体 sharding 算法实现的时候解决,当然,直接将本issue作为解决也可以

heroyf commented 1 year ago

这个问题是在实现增强的 ShardingSelector 留下的,主要考虑到的是如果直接在Sharding里执行广播,那 会直接影响findDstByPredicate里 or 分支的逻辑,考虑到这点的话当时是想把这个问题留到具体 sharding 算法实现的时候解决,当然,直接将本issue作为解决也可以

实际上Or分支,就是会了获取left和right各自分片再去重后的并集,当left返回广播的结果,right返回广播的结果,mergeOr一下是否就是最终的查询语句。

另外mergeOr代码会出现最终的并集出现重复的查询语句

func (*ShardingSelector[T]) mergeOR(left, right sharding.Result) sharding.Result {
    dsts := make([]sharding.Dst, 0, len(left.Dsts)+len(right.Dsts))
    m := make(map[string]bool, 8)
    for _, r := range right.Dsts {
        for _, l := range left.Dsts {
            if r.NotEquals(l) {
                tbl := fmt.Sprintf("%s_%s_%s", l.Name, l.DB, l.Table)
                if _, ok := m[tbl]; ok {
                    continue
                }
                dsts = append(dsts, l)
                m[tbl] = true
            }
        }
                // 这一步会导致重复的元素添加到最终的结果中
        dsts = append(dsts, r)
    }
    return sharding.Result{Dsts: dsts}
}
Stone-afk commented 1 year ago

问题就在这里,左右两边都要广播并求并集,所以考虑到多层嵌套条件的情况,我在这里是比较谨慎的。

flycash commented 1 year ago

我以为是这里的:

        if col.name == s.meta.ShardingKey {
            shardingDB, err := s.meta.DBShardingFunc(right.val)
            if err != nil {
                return nil, errs.ErrExcShardingAlgorithm
            }
            shardingTbl, err := s.meta.TableShardingFunc(right.val)
            if err != nil {
                return nil, errs.ErrExcShardingAlgorithm
            }
            _, existDB := s.db.DBs[shardingDB]
            if !existDB {
                return nil, errs.ErrNotFoundTargetDB
            }
            _, existTbl := s.db.Tables[shardingTbl]
            if !existTbl {
                return nil, errs.ErrNotFoundTargetTable
            }
            dst := Dst{DB: shardingDB, Table: shardingTbl}
            res = append(res, dst)
        }

在查询条件并不是分库分表键的时候,这里应该返回广播。

Stone-afk commented 1 year ago

@flycash 这里不是之前重构了吗