业务系统如何正确实现防重名功能

2024-04-21 05:08

本文主要是介绍业务系统如何正确实现防重名功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

常见但是错误的实现

在业务系统中防重名是一个非常普遍的需求,例如用户注册时不允许用户名重复、已登录用户不可以在自己的账号范围内创建同名的某种实体等。很多人在实现的时候都是简单的先判断名字是否重复,如果没有则执行插入操作,如下:

    public void register(User user) {// 判断是否重复 (1)if (userMapper.selectExist(user.getUsername())) {// 报错throw new XXXException();}// 执行注册 (2)userMapper.insertSelective(xxxModel);}

实际上这个逻辑存在严重的漏洞,在"高并发"下会出现用户名重复的情况,而且数据库操作执行的越慢,重复的概率就会越高。我们先来分析下原因:

假设有A、B两个用户都使用了同一个叫作"bruce"的用户名,且恰好"同时"点击了注册按钮,请求也恰好"同时"到达服务器。这时候你的web服务会启动两条线程来处理这两个请求,于是A, B同时执行数据库查询 (1) 来判断是否重名,因为此时表中还不存在username列为bruce的行,所以A, B都会顺利通过检查,然后各自插入一条记录,从而导致用户名重复。

我相信在相当多的系统中都会有类似的代码,当然不一定是用户注册的场景。在多数情况下问题并不会被暴露出来,因为你得承认,绝大多数人做的系统QPS都没有"那么"高,毕竟中国BBATJ就这么几家,小公司还是占大多数的。但是站在技术的角度上讲这是一种错误,应该避免。

不能把问题扔给数据库

如何解决?在数据库层面,最简单的就是给username字段加唯一约束,这是最笨的方式,但唯一约束会严重制约扩展能力(读写分离、分库分表),同时降低系统的健壮性(通过数据库报错来反馈业务错误信息)。除了唯一索引,还可以锁表,但是我觉得任何一个有责任心的程序员都不会这样实现。存储过程?No, 不存在的。所以这种业务问题根本就不是DB层应该考虑的事情。

问题抛给业务层。现在的场景是要保证同一时刻,对于同一个用户名只能有一个人注册成功。我们首先想到的是加锁了,synchronized?可以,但是只适用于单例模式,相信在互联网公司没人会这么干,所以我们需要分布式锁。此外还要要注意你不能简单粗暴的把整个注册逻辑用一把锁给锁了,那样会严重降低系统的吞吐量,不可取。想在问题被拆分成了两个,如何实现分布式锁 + 如何尽量减少锁的粒度。

正确实现Redis锁 && 降低锁粒度

实现分布式锁最简单的方式就是用Redis了,但是Redis锁坑很多,一不注意就会有漏洞。正确实现的第一个问题是,判断key是否存在跟创建key(锁)要原子性完成。这个原因非常简单,用户注册就能说明问题。Redis中要想原子性有三种方式,一是利用Redis的单线程特性想办法用一条指令完成任务,如setnx;二是使用Pipeline连续执行多条命令;三是执行lua脚本。显然第一种方法最简单。在Jedis中,我们可以直接使用5个参数的set()方法,如:

// null 表示已经存在
String ret = cmd.set(key, val, 'nx', 'ex', expireSecond);

这个方法能同时完成:

  • 判断key是否存在,存在返回null
  • 创建key, 设置成指定值
  • 指定过期时间

那么问题来了,key和value应该设置成啥才好?这里是要分场景的。

如果你是在做一把全局锁让多个实例都来争抢的话,那么key应该设置成一个固定的值,value设置成一个随机数(nonce)。在释放锁的时候,程序要先判断当前redis里key的value值是不是自己之前设置的nonce, 如果是才能安全的删除key,否则就啥也不干。这是为了防止某个实例因为业务逻辑处理超时而导致意外的释放了别人的锁。我们来分析一下,假如key的过期时间是5s, 然后你在某次执行过程中用了10s才完成处理,那么此时锁有可能会被被人给抢走。如果你在10s后只是简单的删除key来释放锁的话就会把别人的锁给释放了,对吧。

另一种场景是防重,比如用户注册。这种情况下不需要nonce, 即value可以忽略,但是key要设为 前缀 + 用户标识 的组合。这样做的好处是除非多个人都用了同一个用户名来注册,否则他们之间不会产生竞争,从而降低了锁的粒度。

这两种场景应该足够用了。但是这里还有其他要考虑的问题: **如果Redis不可用了怎么办?**你是让程序报错等待人工干预,还是进行“降级”忽略问题?这就要看业务需求了,比如用户注册,如果系统的QPS没那么高的话,redis锁失效可以允许继续注册,因为出现重名的概率很低。无论如何一定要想着redis有连接失败的可能性,这很重要。

End

这篇关于业务系统如何正确实现防重名功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922220

相关文章

MySQL 8 中的一个强大功能 JSON_TABLE示例详解

《MySQL8中的一个强大功能JSON_TABLE示例详解》JSON_TABLE是MySQL8中引入的一个强大功能,它允许用户将JSON数据转换为关系表格式,从而可以更方便地在SQL查询中处理J... 目录基本语法示例示例查询解释应用场景不适用场景1. ‌jsON 数据结构过于复杂或动态变化‌2. ‌性能要

Python实现终端清屏的几种方式详解

《Python实现终端清屏的几种方式详解》在使用Python进行终端交互式编程时,我们经常需要清空当前终端屏幕的内容,本文为大家整理了几种常见的实现方法,有需要的小伙伴可以参考下... 目录方法一:使用 `os` 模块调用系统命令方法二:使用 `subprocess` 模块执行命令方法三:打印多个换行符模拟

SpringBoot+EasyPOI轻松实现Excel和Word导出PDF

《SpringBoot+EasyPOI轻松实现Excel和Word导出PDF》在企业级开发中,将Excel和Word文档导出为PDF是常见需求,本文将结合​​EasyPOI和​​Aspose系列工具实... 目录一、环境准备与依赖配置1.1 方案选型1.2 依赖配置(商业库方案)二、Excel 导出 PDF

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho

使用zip4j实现Java中的ZIP文件加密压缩的操作方法

《使用zip4j实现Java中的ZIP文件加密压缩的操作方法》本文介绍如何通过Maven集成zip4j1.3.2库创建带密码保护的ZIP文件,涵盖依赖配置、代码示例及加密原理,确保数据安全性,感兴趣的... 目录1. zip4j库介绍和版本1.1 zip4j库概述1.2 zip4j的版本演变1.3 zip4

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

Spring StateMachine实现状态机使用示例详解

《SpringStateMachine实现状态机使用示例详解》本文介绍SpringStateMachine实现状态机的步骤,包括依赖导入、枚举定义、状态转移规则配置、上下文管理及服务调用示例,重点解... 目录什么是状态机使用示例什么是状态机状态机是计算机科学中的​​核心建模工具​​,用于描述对象在其生命

Spring Boot 结合 WxJava 实现文章上传微信公众号草稿箱与群发

《SpringBoot结合WxJava实现文章上传微信公众号草稿箱与群发》本文将详细介绍如何使用SpringBoot框架结合WxJava开发工具包,实现文章上传到微信公众号草稿箱以及群发功能,... 目录一、项目环境准备1.1 开发环境1.2 微信公众号准备二、Spring Boot 项目搭建2.1 创建

IntelliJ IDEA2025创建SpringBoot项目的实现步骤

《IntelliJIDEA2025创建SpringBoot项目的实现步骤》本文主要介绍了IntelliJIDEA2025创建SpringBoot项目的实现步骤,文中通过示例代码介绍的非常详细,对大家... 目录一、创建 Spring Boot 项目1. 新建项目2. 基础配置3. 选择依赖4. 生成项目5.