基于go-zero的微服务即时通讯项目day1——用户业务

今天完成了用户相关业务的api及rpc的设计和实现。业务方面很简单,包括注册、登录、获取信息、查找用户,都很好实现,就不写了。主要记录我遇到的一个坑,一度让我怀疑是go-zero的问题,最终在看了几个小时源码之后确定了是自己的问题。不过也不亏,起码学习了go-zero中对于缓存处理的实现。

问题出在注册业务上,我的rpc内注册逻辑是这样写的:

func (l *RegisterLogic) Register(in *user.RegisterReq) (*user.RegisterResp, error) {
    //验证用户是否注册过
    userEntity, err := l.svcCtx.UsersModel.FindOneByPhone(l.ctx, in.Phone)
    fmt.Println("userEntity", userEntity, err)
    if err != nil && !errors.Is(err, models.ErrNotFound) {
        return nil, errors.Wrapf(err, "查找用户失败,错误:%v,手机号:%v", err, in.Phone)
    }
    if err == nil || userEntity != nil {
        return nil, errors.WithStack(xerr.New(xerr.ServerCommonError, "用户已存在"))
    }

    id := uuid.New().String()

    //创建用户
    userEntity = &models.Users{
        Id:       id,
        Avatar:   in.Avatar,
        Nickname: in.Nickname,
        Phone:    in.Phone,
        Gender: sql.NullInt64{
            Int64: int64(in.Gender),
            Valid: true,
        },
    }

    //密码加密
    if len(in.Password) > 0 {
        hashedPassword, err := encrypt.GenPasswordHash([]byte(in.Password))
        if err != nil {
            return nil, errors.Wrapf(err, "密码加密失败,错误:%v,手机号:%v", err, in.Phone)
        }

        userEntity.Password = sql.NullString{
            String: string(hashedPassword),
            Valid:  true,
        }
    }

    //插入用户
    _, err = l.svcCtx.UsersModel.Insert(l.ctx, userEntity)
    if err != nil {
        return nil, errors.Wrapf(err, "创建用户失败,错误:%v,手机号:%v", err, in.Phone)
    }

    //生成token
    now := time.Now().Unix()
    token, err := ctxdata.GetJwtToken(l.svcCtx.Config.Jwt.AccessSecret, now, l.svcCtx.Config.Jwt.AccessExpire, userEntity.Id)
    if err != nil {
        return nil, errors.Wrapf(err, "生成token失败,错误:%v,用户:%v", err, in.Phone)
    }

    return &user.RegisterResp{
        Token:  token,
        Expire: now + l.svcCtx.Config.Jwt.AccessExpire,
    }, nil
}

总体就是很简单的查找重复用户->插入新用户。但是我在用apipost测试rpc服务时,却发现“查找重复用户”这个功能似乎不好使,可以重复注册很多个手机号相同的用户。(我区分重复用户是基于手机号)

这凭直觉也能想到是缓存的问题,于是我又尝试调试注册,并观察了redis的变化。发现在注册手机号12345后,redis多了一个键12345,对应的值是一个*。

*是go-zero在查询不到数据时,向redis写入的一个值,表示这个key查询不到。于是导致下一次注册12345时,查询返回没有这个用户,于是造成了重复注册。

但是奇怪的点是,按理来讲,执行insert后这条缓存就会被删除,但它现在却保留了。我的models内实现如下(使用goctl生成没有改过):

func (m *defaultUsersModel) FindOne(ctx context.Context, id string) (*Users, error) {
    usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, id)
    var resp Users
    err := m.QueryRowCtx(ctx, &resp, usersIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
        query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", usersRows, m.table)
        return conn.QueryRowCtx(ctx, v, query, id)
    })
    switch err {
    case nil:
        return &resp, nil
    case sqlc.ErrNotFound:
        return nil, ErrNotFound
    default:
        return nil, err
    }
}

func (m *defaultUsersModel) FindOneByPhone(ctx context.Context, phone string) (*Users, error) {
    usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, phone)
    var resp Users
    err := m.QueryRowCtx(ctx, &resp, usersIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
        query := fmt.Sprintf("select %s from %s where `phone` = ? limit 1", usersRows, m.table)
        return conn.QueryRowCtx(ctx, v, query, phone)
    })
    switch err {
    case nil:
        return &resp, nil
    case sqlc.ErrNotFound:
        return nil, ErrNotFound
    default:
        return nil, err
    }
}

func (m *defaultUsersModel) Insert(ctx context.Context, data *Users) (sql.Result, error) {
    usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, data.Id)
    ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
        query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?)", m.table, usersRowsExpectAutoSet)
        return conn.ExecCtx(ctx, query, data.Id, data.Avatar, data.Nickname, data.Phone, data.Password, data.Status, data.Gender)
    }, usersIdKey)
    return ret, err
}

最后在阅读了很久源码之后,发现居然是一个很弱智的问题:

在注册服务开始时,我查找重复用户会调用FindOneByPhone()方法,该方法会查询结果并将结果写入缓存。但此时goctl给我设置的键是:

fmt.Sprintf("%s%v", cacheUsersIdPrefix, phone)

用了ID的前缀加上手机号。然而我们看Insert的实现,里面往ExecCtx内传入的键是:

fmt.Sprintf("%s%v", cacheUsersIdPrefix, data.Id)

因此导致缓存没有被正确删除。将FindOneByPhone()和Insert()按如下修改,问题解决:

func (m *defaultUsersModel) FindOneByPhone(ctx context.Context, phone string) (*Users, error) {
    usersIdKey := fmt.Sprintf("%s%v", cacheUsersPhonePrefix, phone)
    var resp Users
    err := m.QueryRowCtx(ctx, &resp, usersIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
        query := fmt.Sprintf("select %s from %s where `phone` = ? limit 1", usersRows, m.table)
        return conn.QueryRowCtx(ctx, v, query, phone)
    })
    switch err {
    case nil:
        return &resp, nil
    case sqlc.ErrNotFound:
        return nil, ErrNotFound
    default:
        return nil, err
    }
}

func (m *defaultUsersModel) Insert(ctx context.Context, data *Users) (sql.Result, error) {
    usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, data.Id)
    usersPhoneKey := fmt.Sprintf("%s%v", cacheUsersPhonePrefix, data.Phone)
    ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
        query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?)", m.table, usersRowsExpectAutoSet)
        return conn.ExecCtx(ctx, query, data.Id, data.Avatar, data.Nickname, data.Phone, data.Password, data.Status, data.Gender)
    }, usersIdKey, usersPhoneKey)
    return ret, err
}

看来以后用goctl生成代码后,还是要仔细检查一遍啊。

你刚刚浪费了人生中宝贵的几分钟。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇