最新要闻
- 专为AR/VR打造!苹果全新操作系统名为xrOS:有专属应用商店 最早明年推出
- 全球看点:高校花2.4亿把教学课堂搬到了海上 网友羡慕不已
- 全球观速讯丨《暗黑破坏神4》赛季通行证需氪金购买 暴雪重申:不会充钱就变强
- 20点狂欢:淘宝每满200减30、天猫/京东每满300减40
- 厂家称黄桃罐头没药效 网友调侃:黄桃罐头为何成东北人疯狂膜拜的神物
- 世界速读:首个进入太空的人类 宇航员加加林个人档案已解密:34岁死于空难
- 天天即时看!女生高铁录乐器考试被打断:工作人员知道情况后特意给换了地方
- 世界关注:将近30万人预约!有米粉要当小米13 Pro钉子户
- 每日观察!86寸4K巨屏!小米EA Pro 86电视今日开售 5999元
- 比Zen4还火 AMD游戏神U锐龙7 5800X3D史低2199元(首发3099)
- MIUI 14花宠摆件上线!米粉:小时候的QQ农场又回来了
- 每日快报!刘强东分享抗新冠经验:比感冒还轻微 京东将开通热线帮助员工
- 造车大战中360赢麻了?周鸿祎大赞自己投资的哪吒汽车
- 天天快看:整活!MIUI 14桌面大变:支持图标自定义调整
- 迷惑!大妈凌晨骑车撞上路边停放车辆:怒斥司机不开车灯
- 黄桃罐头遭疯抢 厂家实诚回应“没药效” 网友调侃:你不懂
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
快资讯丨用户重复注册分析-多线程事务中加锁引发的bug
本文记录博主线上项目一次用户重复注册问题的分析过程与解决方案
(资料图片)
- 博主github地址:github.com/wayn111
一 复现过程
线上客户端用户使用微信扫码登陆时需要再绑定一个手机号,在绑定手机后,用户购买客户端商品下线再登录,发现用户账号ID被变更,已经不是用户刚绑定手机号时自动登录的用户账号ID,查询线上数据库,发现同一个手机生成了多个账号id,至此问题复现
二 分析过程
发现数据库中一个手机号生成了多个用户账号,第一反应是用户在绑定手机号过程中,多次点击绑定按钮,导致绑定接口被调用多次,造成多线程并发调用用户注册接口,进而生成多个账号。为了验证我们的猜想,直接查看绑定手机后的用户注册方法
/** * 根据用户手机号进行注册操作 */// 启动@Transactional事务注解@Transactional(rollbackFor = Exception.class)public boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp resp) { RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10); boolean lock; try { lock = redisLock.lock(); // 使用redis分布式锁 if (lock) { // 查询数据库该用户手机号是否插入成功,已存在则退出操作 MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes()); if (Objects.nonNull(member)) { resp.setResultFail(ReturnCodeEnum.USER_EXIST); return false; } // 执行用户注册操作,包含插入用户表、订单表、是否被邀请 ... } } catch (Exception e) { log.error("用户注册失败:", e); throw new Exception("用户注册失败"); } finally { redisLock.unLock(); } // 添加注册日志,上报到数据分析平台... return true;}
初看代码,在分布式环境中,先加分布式锁保证同时只能被一个线程执行,然后判断数据库中是否存在用户手机信息,已存在则退出,不存在则执行用户注册操作,咋以为逻辑上没有问题,但是线上环境确实就是出现了相同手机号重复注册的问题,首先代码被 @Transactional
注解包含,就是在自动事务中执行注册逻辑
现在博主带大家回忆一下,MySQL
事务的隔离级别有4个
- Read uncommitted:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。
- Read committed:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。
- Repeatable read:可重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。
- Serializable:串行化,一个事务一个事务的执行。
- MySQL数据库默认使用可重复读( Repeatable read)。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,MySQL的默认隔离级别是读可重复读。在上述场景里,也就是说,无论其他线程事务是否提交了数据,当前线程所在事务中看到的数据值始终不受其他事务影响
说人话(划重点):就是在 MySQL
中一个线程所在事务是读不到另一个线程事务未提交的数据的
下面结合上述代码给出分析过程:上述注册逻辑都包含在 Spring
提供的自动事务中,整个方法都在事务中。而加锁也在事务中执行。最终导致我们注册 线程B
在当前事物中查询不到另一个注册 线程A
所在事物未提交的数据, 举个例子
eg:
- 当用户执行注册操作,重复点击注册按钮时,假设线程A和B同时执行到
redisLock.lock()
时,假设线程A获取到锁,线程B进入自旋等待,线程A执行mapper.findByMobile(body.getAccount(), body.getRegRes())
操作,发现用户手机不存在数据库中,进行注册操作(添加用户信息入库等),执行完毕,释放锁。执行后续添加注册日志,上报到数据分析平台操作,注意此时事务还未提交。
- 线程B终于获取到锁,执行
mapper.findByMobile(body.getAccount(), body.getRegRes())
操作,在我们一开始的假设中,以为这里会返回用户已存在,但是实际执行结果并不是这样的。原因就是线程A的事务还未提交,线程B读不到线程A未提交事务的数据也就是说查不到用户已注册信息,至此,我们知道了用户重复注册的原因。
三 解决方案:
给出三种解决方案
3.1 修改事务范围,将事务的操作代码最小化,保证在加锁结束前完成事务提交,代码如下开启手动事务,这样其他线程在加锁代码块中就能看到最新数据
@Autowiredprivate PlatformTransactionManager platformTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;private boolean userRegister(LoginReqBody body, BaseReqHeader header, BaseResp resp) { RedisLock redisLock = redisCache.getRedisLock(RedisNameEnum.USER_REGISTER_LOCK.get(""), 10); boolean lock; TransactionStatus transaction = null; try { lock = redisLock.lock(); // 使用redis分布式锁 if (lock) { // 查询数据库该用户手机号是否插入成功,已存在则退出操作 MemberDO member = mapper.findByMobile(body.getAccount(), body.getRegRes()); if (Objects.nonNull(member)) { resp.setResultFail(ReturnCodeEnum.USER_EXIST); return false; } // 手动开启事务 transaction = platformTransactionManager.getTransaction(transactionDefinition); // 执行用户注册操作,包含插入用户表、订单表、是否被邀请 ... // 手动提交事务 platformTransactionManager.commit(transaction); ... } } catch (Exception e) { log.error("用户注册失败:", e); if (transaction != null) { platformTransactionManager.rollback(transaction); } return false; } finally { redisLock.unLock(); } // 添加注册日志,上报到数据分析平台... return true;}
3.2 在用户注册时针对注册接口添加防重复提交处理
下面给出一个基于 AOP
切面 + 注解实现的限流逻辑
/** * 限流枚举 */public enum LimitType { // 默认 CUSTOMER, // by ip addr IP}/** * 自定义接口限流 * * @author jacky */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Limit { boolean useAccount() default true; String name() default ""; String key() default ""; String prefix() default ""; int period(); int count(); LimitType limitType() default LimitType.CUSTOMER;}/** * 限制器切面 */@Slf4j@Aspect@Componentpublic class LimitAspect { @Autowired private StringRedisTemplate stringRedisTemplate; @Pointcut("@annotation(com.dogame.dragon.sparrow.framework.common.annotation.Limit)") public void pointcut() { } @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attrs.getRequest(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method signatureMethod = signature.getMethod(); Limit limit = signatureMethod.getAnnotation(Limit.class); boolean useAccount = limit.useAccount(); LimitType limitType = limit.limitType(); String key = limit.key(); if (StringUtils.isEmpty(key)) { if (limitType == LimitType.IP) { key = IpUtils.getIpAddress(request); } else { key = signatureMethod.getName(); } } if (useAccount) { LoginMember loginMember = LocalContext.getLoginMember(); if (loginMember != null) { key = key + "_" + loginMember.getAccount(); } } String join = StringUtils.join(limit.prefix(), key, "_", request.getRequestURI().replaceAll("/", "_")); List strings = Collections.singletonList(join); String luaScript = buildLuaScript(); RedisScript redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long count = stringRedisTemplate.execute(redisScript, strings, limit.count() + "", limit.period() + ""); if (null != count && count.intValue() <= limit.count()) { log.info("第{}次访问key为 {},描述为 [{}] 的接口", count, strings, limit.name()); return joinPoint.proceed(); } else { throw new DragonSparrowException("短时间内访问次数受限制"); } } /** * 限流脚本 */ private String buildLuaScript() { return "local c" + "\nc = redis.call("get",KEYS[1])" + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + "\nreturn c;" + "\nend" + "\nc = redis.call("incr",KEYS[1])" + "\nif tonumber(c) == 1 then" + "\nredis.call("expire",KEYS[1],ARGV[2])" + "\nend" + "\nreturn c;"; }}
3.3 前端针对绑定手机按钮添加防止连点处理
四 总结
线上项目对于 Spring
提供的自动事务注解使用要多加思考,尽可能减少事务影响范围,针对注册等按钮要在前后端添加防重复点击处理
快资讯丨用户重复注册分析-多线程事务中加锁引发的bug
【焦点热闻】PTA作业6-8电信系列总结
环球要闻:C#中的WebAPI
专为AR/VR打造!苹果全新操作系统名为xrOS:有专属应用商店 最早明年推出
全球看点:高校花2.4亿把教学课堂搬到了海上 网友羡慕不已
2022-6.824-Lab2:Raft
007爬虫之requests模块进阶
全球今日讯!攻防世界-unseping(序列化,Bash shell)
全球观速讯丨《暗黑破坏神4》赛季通行证需氪金购买 暴雪重申:不会充钱就变强
JavaScript:this指针
20点狂欢:淘宝每满200减30、天猫/京东每满300减40
厂家称黄桃罐头没药效 网友调侃:黄桃罐头为何成东北人疯狂膜拜的神物
全球看点:验证jenkins自动化部署
世界速读:首个进入太空的人类 宇航员加加林个人档案已解密:34岁死于空难
天天即时看!女生高铁录乐器考试被打断:工作人员知道情况后特意给换了地方
2021冬--简单描述时间复杂度
PDF转图片
Python实战案例,tkinter+random模块,实现课堂随机抽选提问并语音播报学生姓名
世界关注:将近30万人预约!有米粉要当小米13 Pro钉子户
每日观察!86寸4K巨屏!小米EA Pro 86电视今日开售 5999元
比Zen4还火 AMD游戏神U锐龙7 5800X3D史低2199元(首发3099)
MIUI 14花宠摆件上线!米粉:小时候的QQ农场又回来了
每日快报!刘强东分享抗新冠经验:比感冒还轻微 京东将开通热线帮助员工
Windows OpenGL ES 图像透明度
热门看点:我“采访”了 ChatGPT
观察:Blazor和Vue对比学习(进阶.路由导航二):布局(母版/嵌套)
世界简讯:WPF内嵌Http协议的Server端
造车大战中360赢麻了?周鸿祎大赞自己投资的哪吒汽车
天天快看:整活!MIUI 14桌面大变:支持图标自定义调整
当前速读:post表单数据格式完全解析multipart/form-data(C#实现)
队列之王: Disruptor 原理、架构、源码 一文穿透
每日视讯:Python如何导入自定义模块?
环球通讯!进制转换和计算机存储规则
SpringBoot整合Swagger2在线文档
迷惑!大妈凌晨骑车撞上路边停放车辆:怒斥司机不开车灯
黄桃罐头遭疯抢 厂家实诚回应“没药效” 网友调侃:你不懂
环球观焦点:曝Redmi K60系列依然将采用塑料支架 为了性价比?
注意!鲍鱼被列入濒危物种红色名录:20种或灭绝
10.3寸墨水屏!联想YOGA Paper墨水平板今日开售 3699元
天天热点评!环形队列、 条带环形队列 Striped-RingBuffer (史上最全)
梅西进4强 雷军点赞:阿根廷门将神了 太给力
全球微速讯:苹果要大赚!消息称iPhone 15又涨价 高端万元起:华为Mate60等跟涨吗
天天快资讯:深度学习基础课:最大池化层的前向传播推导
阿根廷点球大战淘汰荷兰:梅西点射破门 追平巴蒂纪录网友纷祝贺
国际领先!中国天眼获得银河系气体高清图像:揭露恒星诞生到消亡
世界热讯:巴西点球大战3-5克罗地亚!无缘四强 内马尔赛后痛哭
今日播报!Python装饰器与迭代器的学习教程
全球快资讯:你必须记住的30个CSS选择器?
最轻折叠屏OPPO Find N2来了!朱海舟:上手后你会WOW一下
当前热点-负债585.68亿、工资发不出:国美获黄光裕公司贷款1.5亿港元
天天快讯:NVIDIA力推的光追版《传送门》游戏被指代码糟糕:AMD显卡坑了
零百加速3.8秒马力暴躁 蔚来全系车型试驾会郑州站开启
世界短讯!MAUI新生5.2-样式外观:控件状态样式VisualState
python实现简单的商品数据管理系统
全球动态:asp.net core 基于Cookies的认证,自定义认证方案
全球新消息丨nginx中的正则表达式,location路径匹配规则和优先级
当前观点:后矿难时代 显卡价格仍居高不下
环球通讯!Java校验自定义枚举值
全球观天下!第一百一十一篇:基本引用类型Date
天天快消息!布洛芬不用抢 中国产能全球第一:一家公司就够33亿人用
天天短讯!BLOG-3总结
世界即时:突发!特斯拉中国工厂将停产Model Y
世界速看:一箱油能跑1200公里!比亚迪护卫舰07上市 20.28万起
当前通讯!雷军再谈小米13徕卡影像:非常自豪 你一定会被震撼到
【天天时快讯】我 一个程序员 靠玩ChatGPT年薪210万
世界看点:DX9性能大涨80% Intel驱动打鸡血原因找到了:做法很聪明
关注:南京一外卖小哥逆行撞劳斯莱斯 网友:几十年外卖白送了
今日快看!全球首架C919正式交付 中国搞这款大飞机有多不容易?
AI画作拍出110万高价创纪录!实测百度AI作画 效果惊艳
主人吃螺蛳粉:猫咪被臭到自闭
世界观速讯丨19岁少女无法走路 竟是因为它?国家早已明令禁止!
rsync远程同步
世界观焦点:AMD RX 7900 XTX渲染跑分性能曝光:感觉和RTX 4080两个时代
【热闻】国内电影票房已达285亿 《阿凡达2》成救命稻草:高价被指吃相难看
世界热推荐:为什么日本人更健康长寿?这12个“秘诀”值得借鉴
小米13 Pro长焦表现一绝:10cm至无穷远均可合焦
【报资讯】网传辽宁一有轨电车碾压电动自行车 官方回应:系剐蹭、人无碍
netmiko+textfsm自动统计交换机端口模块型号数量与闲置模块
【全球独家】【网关开发】4.Openresty 使用events插件进行事件通知
世界新消息丨蒟蒻颤抖:AI打信奥赛,三分之二赛题一遍过
【求助帖】从技术转为项目经理后,如何快速进入角色?
angr_ctf——从0学习angr(四):库操作和溢出漏洞利用
【世界速看料】新一代广汽本田皓影官图发布:大嘴变方嘴、可选7座
《原神》获TGA“玩家之声”奖!官方发800原石:全体都有
世界新动态:一部车骑10年!绿源推出INNO9-lite电动自行车:新国标 80km续航
天天快消息!NVIDIA发布527.56显卡驱动程序:DLSS 3游戏性能更强了
天天最资讯丨站起来了!哈弗H6插电混动版11月销量首超4000:直逼问界M5
焦点速递!CSS绝对定位7大应用场景实战案例分享
教你用CSS实现表单部件
环球热议:物联网平台在AIoT领域8大场景应用
东航官宣全球第一架C919商业首飞时间!这7大城市有福了
打爆丰田、本田混动SUV 比亚迪护卫舰07上市:20.28万起
天天微资讯!换代!AMD锐龙9 7950X3D来了:游戏性能比酷睿i9-13900K高出33%
【速看料】全国5G网络接入速率出炉:北京、上海都没抢到第一 移动最快
【世界新要闻】上线7年无敌手!《王者荣耀》11月吸金超13亿元:蝉联销冠
观点:MYSQL 1 DAY
世界微速讯:智能PDU,网络远程管理电源能耗提升配电效率
当前滚动:“云办公”如何用任务协同工具搞定项目和团队管理?
今日聚焦!SSM整合(spring-springmvc-mybatis)之CRUD
焦点日报:又拓新业务 比亚迪全新皮卡谍照曝光:DM混动没跑了