记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

开心一刻

有一个问题一直困扰着我:许仙选择了救蛇,但为什么杨果选择了救鹰(而不是救蛇)。

[En]

A question has been bothering me: Xu Xian chose to save the snake, but why Yang Guo chose to save the eagle (instead of saving the snake).

想了想,其实,杨果救鹰是有原因的。当鹰与蛇搏斗时,

[En]

After thinking about it, in fact, Yang Guo saved the eagle for a reason. When the eagle fought with the serpent,

鹰对杨果说:杀蛇,杀蛇!

[En]

The eagle said to Yang Guo: kill snakes, kill snakes!

蛇对杨果说:杀鹰,杀鹰!

[En]

The snake said to Yang Guo: kill eagles, kill eagles!

杨过果断选择了杀蛇

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

业务场景

业务描述

在商业上有这样的需要。张三和李思这两个用户,如果相互关注,就会成为朋友。

[En]

There is such a need in business. Zhang San and Li Si, two users, will become friends if they follow each other.

设计上有两张表,关注关系表: tbl_follow

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

朋友关系表: tbl_friend

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

我们以张三对李思的关注为例。业务实现流程如下所示。

[En]

Let’s take Zhang San’s attention to Li Si as an example. The business implementation process is like this.

1、先查询李四有没有关注张三

2、如果李四关注了张三,则成为好友,往 tbl_friend 插入一条记录;如果李四没有关注张三,则只是张三单向关注李四,往 tbl_follow 插入一条记录

看似没问题,但如果从并发性的角度来看,还正常吗?

[En]

It seems no problem, but if we look at it from the perspective of concurrency, is it still normal?

如果张三和李思同时关注对方,那么第一步业务执行过程的结果可能是双方都不关注对方(加数据库独占锁没用,记录不存在,行锁不生效)

[En]

If Zhang San and Li Si pay attention to each other at the same time, the result of the first step of the business implementation process may be that neither side pays attention to each other (adding the exclusive lock of the database is useless, the record does not exist, and the row lock does not take effect)

结果是,张三关注李思,李思关注张三,但张三和李思没有成为朋友,导致不合规的业务需求!

[En]

The result is that Zhang San pays attention to Li Si and Li Si pays attention to Zhang San, but Zhang San and Li Si do not become friends, which leads to non-compliance with business needs!

问题复现

相关环境如下

MySQL : 5.7.21-log ,隔离级别 RR

Spring Boot : 2.1.0.RELEASE

MyBatis-Plus : 3.1.0

核心代码如下

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

完整代码见:mybatis-plus-demo

我们来复现下问题

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

正确结果应该是: tbl_follow 、 tbl_friend 中各插入一条记录

但目前的结果是只往 tbl_follow 中插了两条记录

如何处理这个问题?欢迎您在评论区留下评论。

[En]

How to deal with this problem? you are welcome to leave comments in the comment area.

JVM 锁

既然并发了,那就加锁呗

JVM 自带的 synchronized 和 Lock 都有同步作用,我们以 synchronized 为例,来看看效果

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

tbl_follow 和 tbl_friend 中各插入一条记录,问题得到解决!

但它是完美的吗?如果项目部署在集群中,而张三和李思关注对方的请求落在集群中的不同节点上,会不会出现交不上朋友的问题?

[En]

But is it perfect? If the project is deployed in a cluster, and Zhang San and Li Si pay attention to each other’s requests fall on different nodes in the cluster, will the problem of not becoming friends arise?

分布式锁

因为 JVM 锁只能控制同个 JVM 进程的同步,控制不了不同 JVM 进程间的同步,所有如果项目是集群部署,那么就需要用分布式锁来控制同步了

关于分布式锁,我就不多说了,网上资料太多了,推荐一篇:再有人问你分布式锁,这篇文章扔给他

如果用分布式锁来解决上述案件的问题,房东是不会意识到的,只会强调一个小细节:如何保证张三关注李思,李思关注张三,申请同一把锁。

[En]

If the problem of the above case is solved with a distributed lock, the landlord will not realize it, but will only emphasize a small detail: how to ensure that Zhang San pays attention to Li Si and Li Si pays attention to Zhang San and they apply for the same lock.

以 Redis 实现为例, key 的命名是有规范的,比如:业务名:方法名:资源名,具体到如上的案例中, key 的名称:user:follow:123:456

如果 张三关注李四 申请的 user:follow:123:456 ,而 李四关注张三 申请的是 user:follow:456:123 ,那么申请的都不是同一把锁,自然也就没法控制同步了

所以申请锁之前,需要进行一个小细节处理,将 followId 与 userId 进行排序处理,小的放前面,大的放后面,类似: user:follow:小id:大id

然后可以保证它们申请相同的锁,因此它们可以自然地控制同步。

[En]

Then they can be guaranteed to apply for the same lock, so they can naturally control synchronization.

唯一索引

下一个实现并不常见,但很有趣。仔细看看。

[En]

The next implementation is not common, but it’s interesting. Take a closer look.

我们改造一下 tbl_follow ,另取名字 tbl_follow_plus

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

注意字段看字段的描述

tbl_follow 中 user_id 固定为 被关注者 , tbl_follow 中 follower_id 固定为 关注者

tbl_follow_plus 中 one_side_id 和 other_side_id 没有固定谁是 关注者 ,谁是 被关注者 ,而是通过 relation_ship 的值来指明谁关注谁

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

业务实现

当 one_side_id 关注 other_side_id 的时候,比较它俩的大小

若 one_side_id < other_side_id ,执行如下逻辑

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

若 one_side_id > other_side_id ,则执行如下逻辑

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

这并不容易理解,让我们只看一下代码实现。

[En]

It’s not easy to understand, let’s just look at the code implementation.

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

执行效果如下

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

我们分析下结果

tbl_follow_plus 只插入了一条记录

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

relation_ship = 3 表示双向关注

tbl_friend 插入了一条记录

记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

同时关注 这个业务就实现了

有小伙伴就有疑问了:楼主你只分析了 one_side_id 关注 other_side_id 的情况,没分析 other_side_id 关注 one_side_id 的情况呀

大家注意看 tbl_follow_plus 表中各个列名的注释, one_side_id 和 other_side_id 并不是具体的 关注者 和 被关注者 ,两者的业务含义是等价的

至于是谁关注谁,是通过 relation_ship 的值来确定的,所以 one_side_id 关注 other_side_id 和 other_side_id 关注 one_side_id 是一样的

至于它是否适用于单向关注,你可以自己核实。

[En]

As to whether it is applicable to one-way concern, you can verify it by yourself.

原理分析

虽然实现了业务需求,但它们很难理解。让我们一步一步地分析它们。

[En]

Although the business requirements are realized, they are difficult to understand. Let’s analyze them step by step.

1、为什么要比较 one_side_id 和 other_side_id 的大小?

tbl_follow_plus 有个唯一索引 UNIQUE KEY uk_one_other (one_side_id,other_side_id)

比较大小的目的就是保证 tbl_follow_plus 的 one_side_id 记录的是小值,而 other_side_id 记录的是大值

例如 123 关注 456 , one_side_id = 123 , other_side_id = 456 , relation_ship = 1

456 关注 123 , one_side_id = 123 , other_side_id = 456 ,但 relation_ship = 2

那这有什么用?

还记得我在上面的 分布式锁 实现方案中强调的那个细节吗

SIZE的作用还在于确保123 Focus 456和456 Focus 123竞争具有行锁的唯一索引。

[En]

The role of size here is also to ensure that 123 focus 456 and 456 focus 123 compete on a unique index with a row lock.

2、insert … on duplicate key update

简单地说:当数据库表中存在记录时,语句在执行时被更新,当记录不存在时,它被插入。

[En]

To put it simply: when a record exists in a database table, the statement is updated when it is executed, and when the record does not exist, it is inserted.

有个前置条件:只能基于唯一索引或主键使用;具体细节可查看:记录不存在则插入,存在则更新 → MySQL 的实现方式有哪些?

insert … on duplicate 确保了在事务内部,执行了这个 SQL 语句后,就占住了这个行锁(先占锁,再执行 SQL)

确保了之后查询 relation_ship 的逻辑是在行锁保护下的读操作

3、relation_ship=relation_ship | 1(relation_ship=relation_ship | 2)

这有点聪明,这里的单词“|”指的是按位或算术。

[En]

This is a bit clever, and the word “|” here refers to bitwise or arithmetic.

relation_ship 的值是在业务代码中指定的,只能是 1 或者 2

因为在 MySQL 层面有个唯一索引的 行锁 ,所以 123 关注 456 和 456 关注 123 的事务之间存在锁竞争,必定是串行的

3.1 若先执行 123 关注 456 的事务, relation_ship 传入的值是 1,事务执行完之后, relation_ship 的值等于 1 | 1 = 1 ;

再执行 456 关注 123 的事务, relation_ship 传入的值是 2,事务执行完之后, relation_ship 的值等于 1 | 2 = 3

3.2 若先执行 456 关注 123 的事务, relation_ship 传入的值是 2,事务执行完之后, relation_ship 的值等于 2 | 2 = 2 ;

再执行 123 关注 456 的事务, relation_ship 传入的值是 1,事务执行完之后, relation_ship 的值等于 2 | 1 = 3

这里也可以看出 relation_ship 的枚举值也不是随意的,当然也可以选择其他的,但是需要满足如上的位运算逻辑

4、insert ignore into friend

简单地说,当记录存在于数据库表中时,它被忽略,而当它不存在时被插入。

[En]

To put it simply, it is ignored when the record exists in the database table and inserted when it does not exist.

也基于主键或唯一索引

[En]

Is also based on the primary key or unique index

另外,在重复调用时,按位或(|)和 insert ignore 可以保证幂等性

总结

1、就文中这个业务而言,唯一索引的实现可读性太差,不推荐大家使用

2、 insert into on duplicate key update 和 insert ignore into 还是比较常见的,最好掌握它们

参考

《MySQL 实战 45 讲》

Original: https://www.cnblogs.com/youzhibing/p/16273601.html
Author: 青石路
Title: 记一次有意思的业务实现 → 单向关注是关注,双向关注则成好友

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/505318/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球