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

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

相关文章

Qt使用QSqlDatabase连接MySQL实现增删改查功能

《Qt使用QSqlDatabase连接MySQL实现增删改查功能》这篇文章主要为大家详细介绍了Qt如何使用QSqlDatabase连接MySQL实现增删改查功能,文中的示例代码讲解详细,感兴趣的小伙伴... 目录一、创建数据表二、连接mysql数据库三、封装成一个完整的轻量级 ORM 风格类3.1 表结构

基于Python实现一个图片拆分工具

《基于Python实现一个图片拆分工具》这篇文章主要为大家详细介绍了如何基于Python实现一个图片拆分工具,可以根据需要的行数和列数进行拆分,感兴趣的小伙伴可以跟随小编一起学习一下... 简单介绍先自己选择输入的图片,默认是输出到项目文件夹中,可以自己选择其他的文件夹,选择需要拆分的行数和列数,可以通过

Python中将嵌套列表扁平化的多种实现方法

《Python中将嵌套列表扁平化的多种实现方法》在Python编程中,我们常常会遇到需要将嵌套列表(即列表中包含列表)转换为一个一维的扁平列表的需求,本文将给大家介绍了多种实现这一目标的方法,需要的朋... 目录python中将嵌套列表扁平化的方法技术背景实现步骤1. 使用嵌套列表推导式2. 使用itert

Python使用pip工具实现包自动更新的多种方法

《Python使用pip工具实现包自动更新的多种方法》本文深入探讨了使用Python的pip工具实现包自动更新的各种方法和技术,我们将从基础概念开始,逐步介绍手动更新方法、自动化脚本编写、结合CI/C... 目录1. 背景介绍1.1 目的和范围1.2 预期读者1.3 文档结构概述1.4 术语表1.4.1 核

在Linux中改变echo输出颜色的实现方法

《在Linux中改变echo输出颜色的实现方法》在Linux系统的命令行环境下,为了使输出信息更加清晰、突出,便于用户快速识别和区分不同类型的信息,常常需要改变echo命令的输出颜色,所以本文给大家介... 目python录在linux中改变echo输出颜色的方法技术背景实现步骤使用ANSI转义码使用tpu

Python使用python-can实现合并BLF文件

《Python使用python-can实现合并BLF文件》python-can库是Python生态中专注于CAN总线通信与数据处理的强大工具,本文将使用python-can为BLF文件合并提供高效灵活... 目录一、python-can 库:CAN 数据处理的利器二、BLF 文件合并核心代码解析1. 基础合

Python使用OpenCV实现获取视频时长的小工具

《Python使用OpenCV实现获取视频时长的小工具》在处理视频数据时,获取视频的时长是一项常见且基础的需求,本文将详细介绍如何使用Python和OpenCV获取视频时长,并对每一行代码进行深入解析... 目录一、代码实现二、代码解析1. 导入 OpenCV 库2. 定义获取视频时长的函数3. 打开视频文

golang版本升级如何实现

《golang版本升级如何实现》:本文主要介绍golang版本升级如何实现问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录golanwww.chinasem.cng版本升级linux上golang版本升级删除golang旧版本安装golang最新版本总结gola

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Mysql实现范围分区表(新增、删除、重组、查看)

《Mysql实现范围分区表(新增、删除、重组、查看)》MySQL分区表的四种类型(范围、哈希、列表、键值),主要介绍了范围分区的创建、查询、添加、删除及重组织操作,具有一定的参考价值,感兴趣的可以了解... 目录一、mysql分区表分类二、范围分区(Range Partitioning1、新建分区表:2、分