最近在搞 Spring Boot ,遇到了这样的一个问题 : 菜鸡如我,想问问大家是如何处理的。
场景 :
有个移动端的用户,手特别快, 注册的时候瞬间连点 100 下 ,发出了 100 个请求 :
问题 :
@Service @Transactional public class UserService { @Autowired UserDao userDao; public boolean register(User user) { Optional<User> user = userDao.findByTelephone(user.telephone); if (user.isPresent()) { throw new IllegalStateException("账号已存在"); } userDao.save(user); return true; } }
register 是非线程安全的 ,这样就导致该用户重复注册,数据库中有多条数据。
我的解决方案 : 加锁... 但是我觉得效率上个问题
大家是如何处理的呢 ?
有篇文章,读了之后有不少收获 :
![]() | 1 realityone 2018-10-25 12:44:26 +08:00 via iPhone 手机号加个唯一索引 |
2 helloworld12 2018-10-25 12:45:45 +08:00 前端设置为短时间内只发送一个请求, 服务端根据填写的信息 hash 计算出唯一 ID |
3 lastpass 2018-10-25 12:49:14 +08:00 via Android ![]() 首先在前端,点击一下之后就 disable,在后台数据没有返回之前不恢复。 后端,加消息队列。 用锁的话,加锁解锁的开销有点大。 |
4 p2pCoder 2018-10-25 12:51:59 +08:00 单机的话 ``` synchronized (user.telephone.intern()) ``` 分布式的话,直接给 给 telephone 用 redis 加锁就行了 性能影响不大吧 |
![]() | 5 HarryQu OP @realityone 我觉得这应该和索引么关系吧. |
![]() | 6 awanabe 2018-10-25 13:01:23 +08:00 @HarryQu unique 索引可以直接在 db 层面上报错了...除了 insert 成功的那条之外..别的都返回 db 错误了... |
![]() | 7 xuanbg 2018-10-25 13:02:25 +08:00 这种非正常的请求,通过限流来防重发就可以了。然后,注册方法中可以增加对手机号是否已存在的判断,如已存在,就返回一个用户已存在的错误信息。 |
![]() | 8 HarryQu OP @lastpass 因为我之前也是做前端的,这种问题,还得后端处理, disable 后, 用户立即退出又重新进入界面,再次提交数据。当然前端可以再次判断,只不过类似场景太多,写起来前端比较麻烦。 消息队列的话,我还真是没用过,我研究下,谢谢。 |
![]() | 9 HarryQu OP @awanabe @realityone 好的 , 我试试。 |
10 jjwjiang 2018-10-25 13:08:49 +08:00 既然 telephone 不能重复,那 DB 里就要 unique 呀,这个 error 完全可以通过 db 抛出来 |
11 IssacTomatoTan 2018-10-25 13:09:28 +08:00 via Android 后端做 体验 安全一次搞定 |
12 lastpass 2018-10-25 13:38:15 +08:00 via Android 回复 @HarryQu 前端防君子不防小人,但能够过滤掉大部分问题。最终还是要后端来处理数据。前后端都要做。 |
![]() | 14 linbiaye 2018-10-25 13:58:00 +08:00 1. 手机号加 unique, save 以后再 select 看看。 |
![]() | 15 linbiaye 2018-10-25 14:02:54 +08:00 |
![]() | 16 reus 2018-10-25 14:03:00 +08:00 当然是数据库加唯一索引 如果这是线上服务,那……祝贵司好运,为啥不找合格的后端来做? |
19 p2pCoder 2018-10-25 15:36:27 +08:00 依据我浅薄的经验来看,把这种幂等性教研扔给 db 的唯一索引来做不合适, 即使有唯一索引,也最好在应用程序中加锁来实现 |
20 ppyybb 2018-10-25 15:49:35 +08:00 via iPhone 数据库做唯一索引,当然二级 key 有唯一索引在 innodb 里面也会增加一点开销,会为插入的时候在二级索引上增加隐式锁(好像是的),但是开销应该较小。 前端做控制,减少大部分误操作了。 如果还不想加唯一的限制(万一有啥奇葩的需求..),那么我觉得每 30 分钟设置一个 redis 的 bloomfilter 去重吧,这样可以保证如果点过了就一定会被发现,没点过才去 db 检查。这样基本可以保证没有任何问题了。(实际上我觉得数据库做唯一索引基本上不可能出现问题了,而且很简单) |
![]() | 21 q397064399 2018-10-25 15:49:40 +08:00 @p2pCoder #19 应用中加锁? 那不是要把用户注册信息加载到 Redis 里面 做分布式锁? |
![]() | 22 ysweics 2018-10-25 15:57:08 +08:00 就像前面说的,手机号码这种字段,数据库里面加 unique 索引,然后代码处理的时候加锁,我觉得单机或者分布式用 redis 锁来解决 |
![]() | 23 metrxqin 2018-10-25 16:01:44 +08:00 这后端太不称职,数据表设计有严重缺陷。 |
24 p2pCoder 2018-10-25 16:39:16 +08:00 @q397064399 如果单机就是 普通的字段加锁就行 上了多台服务,肯定要用 redis 或者 zk 之类对相应字段加锁 我目前做的的都是 一个服务部署多台,用的是 redis 加锁 总之,别把压力抛给 db |
25 Raymon111111 2018-10-25 16:41:35 +08:00 你这叫防并发不是幂等 幂等是用户慢慢的点 100 下, 处理正确 |
26 fkdog 2018-10-25 16:48:32 +08:00 首先 lz 一堆人把幂等和滤重混淆在一起了。什么是幂等?举个例子,就是一堆下订单的重复请求到达服务器,对所有的订单接口都应该返回一摸一样的订单号数据。 所以对于幂等要求,正确的做法应该是,接到请求以后去数据库里查询,是否已经存在相关订单数据,如果是,则直接返回已有的订单数据详情。 当然这个需要考虑一个情况,对于同一个商品,可能用户的确是手动下了两个一样的订单,所以需要判断是否是一定时间间隔范围内的。可以使用时间戳、唯一 token 等方式滤重。 另外,确保订单入库的过程在同一个事物中。 |
28 foolish1024 2018-10-25 16:49:18 +08:00 乐观锁+唯一性索引 |
![]() | 29 richieboy 2018-10-25 16:51:55 +08:00 防并发就是要锁吧,数据库为什么不能重复唯一值,应该也是数据库自己加了锁才实现的吧?我们做的就是根据实际情况尽可能选择轻量级的锁 |
30 p2pCoder 2018-10-25 17:13:05 +08:00 @tabris17 单机的话的直接对电话号码字段加锁就行 如果部署多台分布式的话,肯定就代表业务的并发正在或者以后潜在面临挑战,在这种背景下,把压力扔给 db 不合适,数据库的唯一索引校验,也是给数据库压力,当然如果 db 能接受,数据库唯一索引也行 这个唯一索引做幂等的本身的扩展性也是问题,当前业务下,我们是对 电话号码添加 unique 校验,但是后来 需要的是限制 姓名+电话号码,来防止重复注册,再到线上的已经膨胀的表中更改索引是不可取的,大表加个索引就可能影响整个 db 如果是 单机的话,synchronized (user.telephone.intern())就行了,也不用考虑分布式锁带来的问题 这为老兄的 user.telephone 写法也是很不讲究 |
![]() | 32 mmdsun 2018-10-25 18:48:37 +08:00 via Android 数据库加唯一索引就 OK 了 |
33 ppyybb 2018-10-25 20:15:46 +08:00 via iPhone @fkdog 对于下单,不可能幂等,因为第一次一定会带来状态的变化。准确说这个接口是安全非幂等的。只是给建议的人知道楼主的实际需求所以没有纠结在这个概念上而已。去重显然能直接实现安全性 |
![]() | 34 519718366 2018-10-25 20:40:38 +08:00 @Raymon111111 正解,感觉前 24 楼,全跑偏了 |
![]() | 35 519718366 2018-10-25 20:44:58 +08:00 ![]() 这是 leader 之前分享过的一个幂等性文章 https://blog.csdn.net/jks456/article/details/71453053 做的 b2b,没什么并发压力,在这块比较弱,不能回答你的问题,只能贴上这个文章了 |
36 luozic 2018-10-25 21:34:02 +08:00 via iPhone 单个 Ip 地址限制请求频率… 这不是网关默认有的功能? |
![]() | 37 passerbytiny 2018-10-26 09:24:32 +08:00 我看了你的代码,在已启动事务,并且查找到账户已存在的时候抛出"账号已存在",那么已经够了,用户点击 100 下,只有第一下回成功,后面的全部是“账号已存在”、“事务检测到脏数据”,或者“事务等待超时”。 你这情况,应该找数据库的问题,代码没有任何问题。 |
38 yc8332 2018-10-26 11:31:04 +08:00 数据库唯一必须的。然后就是接口是加锁的,不知道你说的效率什么意思。就是 mc/redis add 就能搞定了。。。还有就是前端要限制,尽量避免垃圾请求 |