如何避免相互加好友时出现的数据库竞争插入好友关系? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
zjsxwc
V2EX    程序员

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

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

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

    数据库好友关系( 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-0-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     Solana     1501 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 47ms UTC 16:32 PVG 00:32 LAX 08:32 JFK 11:32
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86