0
点赞
收藏
分享

微信扫一扫

pg实现并发相互喜欢

Raow1 2022-02-13 阅读 30

pg实现并发相互喜欢

背景

在极客时间上阅读到15 | 答疑文章(一):日志和索引相关问题一章时, 有一个很有趣的并发问题. 作者给出了mysql版本的答案.

因为平时pg用的比较多, 所以使用pg进行实现了一下.

具体问题

类似探探类的交友软件, 用户Bob对喜欢的人Alice, 可以点击喜欢按钮, 此时Bob会关注Alice,
如果, Alice也喜欢Bob, 构成了Bob, Alice相互喜欢, 则成为好友.

设计上有3个表, users, likers, friends.
当用户Bob 喜欢Alice时, likers 里面创建一条记录, 然后判断是否存在 Alice喜欢Bob的记录, 如果有, 则在friends表中插入一条记录.

具体表结构如下.

CREATE TABLE users(
   id SERIAL,
   username VARCHAR,
   PRIMARY KEY(id)
);

CREATE TABLE likers (
  id SERIAL,
  user_id  integer NOT NULL,
  liker_id integer NOT NULL,
  PRIMARY KEY (id)
);

create unique index uk_user_id_liker_id on likers (user_id,liker_id);

CREATE TABLE friends (
  id SERIAL,
  friend_1_id integer NOT NULL,
  friend_2_id integer NOT NULL,
  PRIMARY KEY (id)
);

create unique index uk_friend on friends (friend_1_id,friend_2_id);

初始化一些数据

insert into users values(1, 'Bob');
insert into users values(2, 'Alice');

上述表结构, 假设 Bob, Alice 同时喜欢对方, 则有可能出现, 双方都在likers表中, 但是却没有生成friends的记录.
事务的隔离级别为 读已提交.

场景复现:

请添加图片描述

Session 1

begin
-- T1 Bob 喜欢 Alice
insert into likers(user_id, liker_id)  values(1, 2) ;
-- T2  看看是否有 Alice 喜欢 Bob 的记录
select * from likers where user_id = 2 and liker_id = 1; 
-- 没有找到, 不执行
-- insert into friends(friend_1_id, friend_2_id) values(1, 2); 
commit;

Session 2

begin
-- T3 Alice 喜欢 Bob
insert into likers(user_id, liker_id) values(2, 1);
-- T4 看看是否有 Alice 喜欢 Bob 的记录
select * from likers where user_id = 1 and liker_id = 2; 
-- 因为Session 1 的事务还没有提交, 找不到数据, 因此不会执行
-- insert into friends(friend_1_id, friend_2_id) values(1, 2); 
commit;

解决方案

如果是为了解决这个问题, 可行的方案有2种

方案一

使用消息队列等异步消息中间件, 当有用户点击喜欢时, 可以先执行插入likers表的逻辑.
然后, 过5秒去执行查询喜欢记录, 生成friends表的逻辑.

只有在极端情况下, 可能有无法生成friends表. 不过, 可以再通过异常处理, 保证一定会执行即可.

方案二

可以在likers表加一个relationship字段表示, 喜欢关系.

  • 值是 1 的时候,表示 user_id 关注 liker_id
  • 值是 2 的时候,表示 liker_id 关注 user_id
  • 值是 3 的时候,表示互相关注

为了代码演示和说明方便, 再创建一个新的 likers_2

CREATE TABLE likers_2 (
  id SERIAL,
  user_id  integer NOT NULL,
  liker_id integer NOT NULL,
  relationship smallint NOT NULL,
  PRIMARY KEY (id)
);
create unique index uk_user_id_liker_2_id on likers_2 (user_id,liker_id);

后面有用到PG的ON CONFLICT, 表示当有主键或者排它性约束报错时, 执行什么内容.
和MySQL的 on duplicate 类似.

特别注意: likers_2 表插入user_id,liker_id时, 一定要确保 id小的是user_id, id大的是liker_id, 这样才能确保在同时喜欢对方时, 去修改同一条数据. 出现行锁. 从而保证数据的一致性.

当Bob(id = 1) 喜欢Alice(id = 2)时

begin
INSERT INTO likers_2 (user_id, liker_id, relationship)
-- 插入user_id,liker_id时, 一定要确保 id小的是user_id, id大的是liker_id
VALUES (1, 2, 1)
ON CONFLICT (user_id, liker_id) DO 
-- 下面详细解释
UPDATE SET relationship = likers_2.relationship | 1;

SELECT relationship FROM likers_2 WHERE user_id = 1 AND liker_id = 2;
-- 如果查询到的 relationship 结果是3, 则执行下面的语句
-- 否则, 不执行
INSERT INTO friends(friend_1_id, friend_2_id) values(1, 2)
ON CONFLICT (friend_1_id, friend_2_id) DO NOTHING;
commit;

| 表示位运算的或, 当插入数据冲突时, 表示已经有Bob和Alice的喜欢关系,

喜欢关系可能是Bob喜欢Alice(relationship = 1) , 也有可能是Alice喜欢Bob(relationship = 2),

因此冲突时使用 likers_2.relationship | 2

  • 1 | 1 = 1 表示: 之前关系是Bob喜欢Alice, 因此值不变.
  • 1 | 2 = 3 表示, 之前关系是Alice喜欢Bob, 而本次值是 1, 表示Bob喜欢Alice, 构成了双方相互喜欢, 值需要为3.

同理类似, 当Alice(id = 2) 喜欢Bob(id = 1)时

begin
INSERT INTO likers_2 (user_id, liker_id, relationship)
VALUES (1, 2, 2)
ON CONFLICT (user_id, liker_id) DO 
UPDATE SET relationship = likers_2.relationship | 2;

SELECT relationship FROM likers_2 WHERE user_id = 1 AND liker_id = 2;

INSERT INTO friends(friend_1_id, friend_2_id) values(1, 2)
ON CONFLICT (friend_1_id, friend_2_id) DO NOTHING;
commit;

具体情况, 可以看图:

请添加图片描述

举报

相关推荐

0 条评论