一个隐藏的并发问题

前言

最近一直在学习高并发的知识,然后我就想,业务代码中哪些地方会用到高并发呢?可是手头上也没有啥项目啊,然后我就想到了我最近做的一个毕业设计,就是一款情侣app的后端系统。我突然想到一个问题,就是一个用户只能与另一个用户绑定为情侣,那么如果有这样的场景,就是有一千个用户同时向后端发送一个请求申请与用户A绑定为情侣关系,那么会出现什么样的情况呢?这个之前倒是没有想过。因为系统设计的是如果一个用户申请或被申请与另外一个用户绑定为情侣关系,如果对方没有拒绝申请则其它用户是不能再申请与该用户绑定为情侣关系的。

模拟场景

为了简单,我找到了处理情侣绑定请求的Controller,并在处理绑定请求的方法中模拟了并发,因为数据库情侣表我并没有设计使用用户的id做主键,所以使用当前发送请求的用户模拟一千个用户同时向目标用户添加一条申请绑定情侣关系的记录,修改后的代码如下。

代码中创建了一个线程池,并循环向线程池中提交一个可以获取返回值的任务Callable,这里我使用了Executors.newCachedThreadPool,为什么使用newCachedThreadPool方法来创建线程池?因为newCachedThreadPool创建的是一个没有核心线程的线程池,就是让所有提交的任务都马上创建一个线程来执行,而不让任何一个任务在队列中等待,这样才贴近一千个用户的并发场景。

编译运行项目,将数据库中情侣表的数据清空,然后发送一个绑定请求,当请求执行完成之后到数据库中查看有多少条数据被添加到了数据中,结果居然有6条记录。

通过数据库建立唯一索引和设置事务的隔离级别解决

发现问题肯定得要解决问题啊,那如何解决这个问题呢?

设置事务的传播方式和事务的隔离级别?首先说的是事务的传播方式,此并发模拟调用service都是在不同线程工作的,所以也就不存在事务的传播问题,你将propagation事务设置为REQUIRES_NEW也无济于事。其次是事务的隔离机制,这里是每个事务都执行插入操作,就算你将事务的隔离级别设置为SERIALIZABLE串行化操作也是无济于事的,因为情侣记录表的主键是自增主键,也没有设置什么将两个用户的id设置为唯一索引或者联合主键,所以就算将隔离级别设置为串行化,记录的新增还是成功的,居然说到了这里,那么我们可以先暂时修改表的结构,给表添加一个唯一索引试试。

将man_id和women_id作为唯一索引。

修改事务

主要是将事务的隔离级别设置为可重复读级别,REPEATABLE_READ幻读的问题InnoDB已经解决了,我们也不需要配置,因为默认的隔离级别也就是REPEATABLE_READ。

执行n次的结果都是只有一条记录是成功插入的。

这其实原本也就是表的设计不合理造成的。我想这是最优的解决方法,因为现实中该业务功能也不可能存在高并发的情况,除非对方是女优吧。所以具体问题还得具体分析,但是这里为了说明并发的处理我们还将以此继续寻找解决方案,可能真的会遇到相似的需求呢。

通过修改业务层的代码加入锁机制

首先将前面创建的唯一索引去掉,建立的外键约束也不要设置为唯一索引。

常规方式,加锁,这里我用synchronized锁住loverDao,因为在spring的管理下service是单例的,dao也是单例的,而操作数据库添加情侣记录的dao是loverDao。

然而,理想是好的,现实却是残酷的,数据库中还是插入了多条数据。

结论:Synchronized和@Transactional 使用时,同步失效。 因为方法上使用了@Transactional 注解声明了事务,所以Synchronized同步失效了,至于为什么失效,大家可以去百度,因为我怕我给的是错误答案。

既然Synchronized行不通,那我换成Lock总行吧?试试。

项目跑起来,看看运行结果

奇了怪了,不管是Synchronized还是Lock,遇到@Transactional都是无效的,这个问题值得研究,后面有空研究一下,这里就先不展开话题了。

将代码拆分为两个方法,将需要声明事务的部分放到第二个方法实现,这样synchronized就可以正常使用了。

虽然实现了并发安全,但这不是可取的方案,因为在该业务中,只是要求同一个用于只能与其它一个用户建立申请绑定记录,而service又是单例的,当其它用户发送请求与别的用户建立申请绑定记录时也会受到影响,这就是将所有用户的请求都变成了串行处理,显然是不可取的。

总结

在设计数据库表的时候如果要求相同内容的记录只存在一条,那么就应用设计好主键、唯一索引这些约束。spring中@Transactional注解的isolation默认值为的Isolation.DEFAULT。

当isolation=DEFAULT时就是选用数据库默认的事务隔离级别,而在mysql中,事务的默认隔离级别是REPEATABLE_READ。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181128G0BZAH00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券