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

2018-02-02 09:52:02 +08:00
 zjsxwc

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

数据库好友关系( 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,那么只能删一次这个好友,这样也不行。

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

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

5989 次点击
所在节点    程序员
35 条回复
sunchen
2018-02-02 10:06:06 +08:00
unique (用户 id,朋友 id )
Soar360
2018-02-02 10:10:14 +08:00
Event Bus 或者说,队列也行。用数据库的唯一索引保证唯一性。话说,好多软件的好友删除都是单方向的啊。
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
2018-02-02 10:33:33 +08:00
account_id,friend_account_id 做唯一索引,幂等操作。
添加或删除好友前先查询数据库,再做 insert 或 update 操作。
nullcc
2018-02-02 10:37:30 +08:00
@Clarencep 改隔离级别理论上是 OK,但是 SERIALIZABLE 这个级别并发性太低了,如果不是有非常严格的顺序要求建议生产环境还是不要用
Clarencep
2018-02-02 10:45:43 +08:00
@nullcc 全局都设置 SERIALIZABLE 肯定不合理,不过数据库隔离级别是可以修改的,而且是可以只改单个会话( session )的,添加好友前改成 SERIALIZABLE,添加后再改回去就行了。

SERIALIZABLE 级别其实就相当于在 MySQL 服务器上搞了个队列。如果 LZ 的应用服务器和 MySQL 是在同一台物理机上,完全没必要自己搞队列,直接用 SERIALIZABLE 级别就行了。当然,如果应用服务器和 MySQL 分开的,或者 MySQL 的资源比较紧张,那肯定最好应用服务器上的队列更佳。
zakokun
2018-02-02 10:48:51 +08:00
没这么麻烦把 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
2018-02-02 10:50:59 +08:00
赞同楼上
zjsxwc
2018-02-02 10:52:29 +08:00
@zakokun #7
@jason2017 #4

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/427902

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX