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

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

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是