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

开心一刻

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

[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)

大家都在看

  • 从“把大象装进冰箱”来谈谈面向对象程序设计

    引子 把大象装进冰箱需要3步:打开冰箱门,把大象装入冰箱,关闭冰箱门。 扩展一下,我们考虑把动物装进冰箱的场景。比如,把猪🐷装进冰箱,把狗🐶装进冰箱,等等。 怎么利用面向对象的思想…

    数据库 2023年6月9日
    0135
  • DB审核查询平台Archery–安装部署可能遇到的问题

    Archery是archer的分支项目,定位于SQL审核查询平台,旨在提升DBA的工作效率,支持多数据库的SQL上线和查询,同时支持丰富的MySQL运维功能,所有功能都兼容手机端操…

    数据库 2023年5月24日
    0114
  • list对象中的数据如何去重呢?

    下文笔者讲述list对象的去重方法分享,list的实现类是我们存储数据的容器, 当里面存储的对象存在重复值时,我们该如何对其进行去重操作呢? 下文笔者将一一道来,首先我们需了解对象…

    数据库 2023年6月11日
    0152
  • Shell文件属性的判断与比较

    Shell支持对文件属性的判断,常用的文件属性操作符很多,如下表所示。更多文件属性操作符可以参考命令帮助手册man test [root@centos7&#xFF5E;]#…

    数据库 2023年6月14日
    0142
  • Jmeter操作ES

    JMeter 是 Apache 组织基于 Java 开发的压力测试工具,用于对软件做压力测试。Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎(简称es…

    数据库 2023年6月14日
    0145
  • idea热部署

    idea热部署 一、修改 pom.xml 文件 修改配置文件 二、打开自动build: File -> Settings -> Build,Exe… -&g…

    数据库 2023年6月16日
    0136
  • 多商户商城系统功能拆解29讲-平台端营销-会员签到

    多商户商城系统,也称为B2B2C(BBC)平台电商模式多商家商城系统。可以快速帮助企业搭建类似拼多多/京东/天猫/淘宝的综合商城。 多商户商城系统支持商家入驻加盟,同时满足平台自营…

    数据库 2023年6月14日
    0151
  • centos安装chrome和解决chrom点击打不开页面的问题

    1、去官网下载chrom的rpm包 2、进入到下载包的目录,执行:yum -y localinstall google-chrome-stable_current_x86_64.r…

    数据库 2023年6月11日
    0141
  • Javascript基础

    作者导言: 引用偶像刘德华的一句话 “学到的就要教人,赚到的就要给人”! 以下是关联的web前端基础知识文章,通过这些文章,您既可以系统地学习和了解这些知识…

    数据库 2023年6月14日
    0163
  • 14 在 Java 中,如何跳出当前的多重嵌套循环

    在最外层添加一个标记如A,然后用breakA,即可跳出多重循环 关键字break 使用范围:switch-case,循环结构中 break在循环结构中的作用:结束 当前循环 bre…

    数据库 2023年6月6日
    0133
  • mybatis SelectKey解析

    1.selectKey介绍及作用 resultType:sql返回的java类型 statementType:STATEMENT|PREPARED|CALLABLE三种默认PREP…

    数据库 2023年5月24日
    0140
  • 源码 | 为金融场景而生的数据类型:Numeric

    高日耀 资深数据库内核研发毕业于华中科技大学,喜欢研究主流数据库架构和源码,并长期从事分布式数据库内核研发。曾参与分布式 MPP 数据库 CirroData 内核开发(东方国信),…

    数据库 2023年5月24日
    0138
  • 第二十章 AOP开发中的坑

    问题 //在同一个业务类中,一个业务方法调用另一个业务方法 //问题: login方法添加有额外功能 // register方法没有添加额外功能 public class User…

    数据库 2023年6月14日
    0121
  • 一份超长的MySQL学习笔记

    前言 最近系统地学习了一边MySQL数据库的基础知识,巩固了一下以前学习的数据库查询基础,又新学习了关于索引、事务等的新内容,做了一些学习笔记。因为MySQL的学习,实操性比较强,…

    数据库 2023年5月24日
    0137
  • 线程本地存储 ThreadLocal

    线程本地存储提供了线程内存储变量的能力,这些变量是线程私有的。 线程本地存储一般用在跨类、跨方法的传递一些值。 线程本地存储也是解决特定场景下线程安全问题的思路之一(每个线程都访问…

    数据库 2023年6月11日
    0133
  • 浅谈事务隔离级别、MVCC及相关特性

    文采不是太好,应该会有地方表达不清楚,烦请指正。 需要事先准备测试表: CREATE TABLE test ( id int(11) NOT NULL, name varchar(…

    数据库 2023年6月16日
    0132
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球