V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zjsxwc
V2EX  ›  程序员

如何避免相互加好友时出现的数据库竞争插入好友关系?

  •  
  •   zjsxwc ·
    zjsxwc · 2018-02-02 09:52:02 +08:00 · 5985 次点击
    这是一个创建于 2481 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写相互加好友时碰到的问题。

    数据库好友关系( friend_relation )表目前是这样的:

    字段:	    id,        account_id,    friend_account_id,       status
    说明:这条好友关系 id,   用户 id,          朋友 id,         状态标记是否被删除	
    

    于是 2 个用户相互添加为好友就会出现对应的 2 条好友关系 friend_relation (这两条好友关系的 account_id 与 friend_account_id 对调)

    那么现在问题来了,如果 2 个用户同时接受对方发起的交友请求,会出现对应的 2 个处理交友的进程都认为双方都没加过好友,于是最后 2 个进程共同插入了 4 条好友关系,而实际上应该插入 2 条就够了。

    加锁好像不行,因为在处理之前连被加锁的数据都没有,根本不能对数据行加锁。

    加唯一 index,应该怎么加,业务上要允许重复多次加删同一个人的好友关系,如果对( account_id, friend_account_id, status )作为一个唯一 index,那么只能删一次这个好友,这样也不行。

    我应该怎么解决这个问题?

    ========== 我现在只能先把插入好友关系这一步工作都放到一个队列里去干

    第 1 条附言  ·  2018-02-02 14:19:46 +08:00
    鉴于业务上直接使用唯一联合索引不太好,用了#16 的办法,增加一个列专门作为唯一索引
    35 条回复    2018-02-05 09:15:26 +08:00
    sunchen
        1
    sunchen  
       2018-02-02 10:06:06 +08:00
    unique (用户 id,朋友 id )
    Soar360
        2
    Soar360  
       2018-02-02 10:10:14 +08:00
    Event Bus 或者说,队列也行。用数据库的唯一索引保证唯一性。话说,好多软件的好友删除都是单方向的啊。
    Clarencep
        3
    Clarencep  
       2018-02-02 10:27:44 +08:00
    提供另外一条思路,MySQL 默认的数据库隔离级别是 REPEATABLE-READ:
    ```
    mysql> select @@global.tx_isolation;

    @@global.tx_isolation
    -----------------------
    REPEATABLE-READ
    ```

    所以即使 LZ 用了事务,也会出现幻读 -- 虽然已经有另外一个事务 B 加了一次好友关系了,但是并发的事务 A 依然认为没有加好友关系。

    而 SERIALIZABLE 级别的隔离可以避免幻读 -- 即可以在开启事务前修改下隔离级别:

    ```
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    ```
    jason2017
        4
    jason2017  
       2018-02-02 10:33:33 +08:00
    account_id,friend_account_id 做唯一索引,幂等操作。
    添加或删除好友前先查询数据库,再做 insert 或 update 操作。
    nullcc
        5
    nullcc  
       2018-02-02 10:37:30 +08:00
    @Clarencep 改隔离级别理论上是 OK,但是 SERIALIZABLE 这个级别并发性太低了,如果不是有非常严格的顺序要求建议生产环境还是不要用
    Clarencep
        6
    Clarencep  
       2018-02-02 10:45:43 +08:00
    @nullcc 全局都设置 SERIALIZABLE 肯定不合理,不过数据库隔离级别是可以修改的,而且是可以只改单个会话( session )的,添加好友前改成 SERIALIZABLE,添加后再改回去就行了。

    SERIALIZABLE 级别其实就相当于在 MySQL 服务器上搞了个队列。如果 LZ 的应用服务器和 MySQL 是在同一台物理机上,完全没必要自己搞队列,直接用 SERIALIZABLE 级别就行了。当然,如果应用服务器和 MySQL 分开的,或者 MySQL 的资源比较紧张,那肯定最好应用服务器上的队列更佳。
    zakokun
        7
    zakokun  
       2018-02-02 10:48:51 +08:00   ❤️ 1
    没这么麻烦把 account_id friend_id 加一个联合唯一索引不就行了? 插入的时候就是这样

    insert into test set account_id =1, friend_account_id =2,state=1 on duplicate key update state=1
    insert into test set account_id =2, friend_account_id =1,state=1 on duplicate key update state=1

    如果数据库没记录就插入,有记录就把 state 变成 1. 这有问题吗
    Immortal
        8
    Immortal  
       2018-02-02 10:50:59 +08:00
    赞同楼上
    zjsxwc
        9
    zjsxwc  
    OP
       2018-02-02 10:52:29 +08:00
    @zakokun #7
    @jason2017 #4

    业务上要求不能联合唯一索引啊,需要保留用户加删了哪些好友的历史记录
    soli
        10
    soli  
       2018-02-02 10:53:57 +08:00
    1 楼的方法不是最简单的么?
    Immortal
        11
    Immortal  
       2018-02-02 10:56:25 +08:00
    @zjsxwc
    你 status 不就是为了标记关系的么,删除添加 update 不就好了,唯一索引里不包含这个字段
    我感觉你钻牛角尖了,休息下再思考吧。。
    winglight2016
        12
    winglight2016  
       2018-02-02 10:58:35 +08:00
    你们的数据库操作不使用 transaction 的吗?
    MiguelValentine
        13
    MiguelValentine  
       2018-02-02 10:59:28 +08:00
    难道不是互相添加才是好友?单方面只算关注? PM 有问题啊
    zakokun
        14
    zakokun  
       2018-02-02 11:02:58 +08:00
    @zjsxwc 这个需要单独的表来解决啊
    好友关系 vs 好友关系历史 应该弄两个单独的表才对 业务类型也不一样的
    好友关系表查询多,好友关系历史表写多查少,一定要区分的
    SmiteChow
        15
    SmiteChow  
       2018-02-02 11:15:40 +08:00
    如果有 ORM 则解决方式为 get_or_create
    gamexg
        16
    gamexg  
       2018-02-02 11:17:26 +08:00   ❤️ 1
    @zjsxwc #9 那专门设置一个字段作为唯一索引,字段值是 好友 1->好友 2-删除时间戳(未删除就是 0)
    zjsxwc
        17
    zjsxwc  
    OP
       2018-02-02 11:21:59 +08:00
    @gamexg #16
    这是个思路,我去试试
    surfire91
        18
    surfire91  
       2018-02-02 11:45:20 +08:00
    @zjsxwc 看了楼主你在#9 的回复,又需要好友关系的状态,又需要加删的记录,不考虑用两个表吗?
    jason2017
        19
    jason2017  
       2018-02-02 13:02:41 +08:00
    @zjsxwc 那就加个日志表啊,肯定不能一张表解决的。
    e9e499d78f
        20
    e9e499d78f  
       2018-02-02 13:05:28 +08:00 via iPhone
    把好友关系设计成双向的不就行了
    sutra
        21
    sutra  
       2018-02-02 13:12:28 +08:00   ❤️ 1
    似乎就我没有看懂问题?

    为什么会出现 4 条记录?最多就 2 条记录呀。
    vjnjc
        22
    vjnjc  
       2018-02-02 13:44:09 +08:00   ❤️ 1
    @sutra
    比如 a 发起添加 b 为好友请求,b 也发起添加 a 请求。
    a 点接受好友(插入 2 条记录好友记录),b 也点接受好友(插入 2 条记录)。

    所以在接受好友的业务里先判断再添加啊!
    sutra
        23
    sutra  
       2018-02-02 13:57:47 +08:00
    @vjnjc 哦,他的意思是,只要一个单向的接受操作,就变为双向的好友呀。那我懂了。

    那就数据库建立唯一键索引,只管往里插入,插入失败就抓起异常看一眼。
    barbery
        24
    barbery  
       2018-02-02 14:07:56 +08:00
    如果要从 db 上处理,unique 的方式是比较好的。如果不从 db 上处理,可以考虑引入第三方的原子性操作的服务,例如 redis 或者队列等。
    eslizn
        25
    eslizn  
       2018-02-02 14:57:58 +08:00
    @Clarencep 事务调整到序列化级别性能下降太厉害了
    Eternallyc
        26
    Eternallyc  
       2018-02-02 15:06:33 +08:00
    比如 a 发起添加 b 为好友请求,b 也发起添加 a 请求。
    a 点接受好友(插入 2 条记录好友记录),b 也点接受好友(插入 2 条记录)。

    所以在接受好友的业务里先判断再添加啊!


    赞同!

    插入的时候判断下 根据用户 id 去关系表里查询 有记录并且 status 字段不是被删除状态 才插入,否则提示 已经是好友
    Clarencep
        27
    Clarencep  
       2018-02-02 15:37:57 +08:00
    @eslizn 可以只改单个会话( session )的隔离级别,添加好友前改成 SERIALIZABLE,添加后再改回去就行了。
    chuanwu
        28
    chuanwu  
       2018-02-02 16:40:03 +08:00
    额,难道就我一个人觉得表设计不合理么...
    zjsxwc
        29
    zjsxwc  
    OP
       2018-02-02 16:47:02 +08:00 via Android
    @chuanwu

    应该怎么设计好?
    vincenttone
        30
    vincenttone  
       2018-02-02 17:18:58 +08:00
    唯一索引 配合上 insert into on duplicate update 即可
    picasso250
        31
    picasso250  
       2018-02-02 21:01:25 +08:00
    有趣的问题.

    难点之所以会出现, 是因为 每个人的 request 会新增两条数据(主动去踩不该踩的坑).

    解决方案: request 的时候,只会增加一条数据. 另一条数据扔给后台的队列(这个队列本身是单线程的)去做.

    完美解决.
    qile1
        32
    qile1  
       2018-02-03 06:40:50 +08:00 via Android
    @vjnjc 你这个直接 a 加 b 为好友,插一条信息,此时 b 是 a 好友,但 a 不是 b 好友,或者可以理解为 b 不把 a 当好友,当 b 在添加 a 为好友时,插入一条数据,这样就不会产生冲突。
    twotiger
        33
    twotiger  
       2018-02-03 16:40:04 +08:00
    @Eternallyc 没用的,并发的话,会同时写进去
    eslizn
        34
    eslizn  
       2018-02-04 16:50:52 +08:00
    @Clarencep 替捉鸡。。。只要有一个会话开启了序列化,其他会话的事务操作都得等待这个会话结束。。。
    Clarencep
        35
    Clarencep  
       2018-02-05 09:15:26 +08:00
    @eslizn 额,居然是这样子的吗?那倒真不如锁表了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3002 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 14:46 · PVG 22:46 · LAX 06:46 · JFK 09:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.