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

常见但是错误的实现

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

    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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容