1.背景:最近在做一個項目的微服務改造工作,遇到了一個需求:用戶根據手機號驗證碼登錄,靜默註冊時用戶名不能重複。
2.分析:之前老項目中使用的是方案是,使用固定字符串+6位隨機數自動生成一個,再將所有的用戶名一次性從DB裏摟出來,循環遍歷,如果有重複則再重新生成一個;若沒有重複則執行插入操作。
很明顯,這樣做在用戶量比較少的情況下是沒有問題的,但是用戶量一旦增大,用戶註冊就能拖垮整個系統,據老員工說,生產環境就曾出現這個問題。另外,還有個問題就是這種最多可以支持的靜默註冊用戶數量最多也就一百萬。
3.解決思路:可以將用戶名的後六位遞增地放入阻塞隊列裏,然後再取出來,這樣既能提高性能,又能保證唯一性。
4.最終解決方案:固定字符串(“一水”)+1位大寫字母(A-Z)+1~6位數字(隊列中取)
(1)解決方案是基於分佈式場景下,即部署了很多用戶服務,使用redis去緩存隊列的初始值和當前使用的字母保證分佈式環境下,不同的JVM內存中隊列中的數字不一樣。
(2)使用線程安全的ArrayBlockingQueue存儲數字下標,單JVM中多線程請求不會出現併發問題。
(3)CommandLineRunner這個作用是項目啓動之後去加載,主要用來初始化隊列中遞增的數字。
(4)當一個字母比如A所在的百萬序列數字用完之後,就使用B,又可以使用百萬序列數字,這種最多可以提供27*100W的不重複的用戶名,一般很少有幾個公司能做到如此大的規模, 當然如果用戶量更大的化,稍加修改就可以增大量,比如使用兩位字母或者大小寫結合,這樣就又多了好多可用用戶名。
圖示:
5.Coding:我把功能封裝在一個工具類裏,配合springboot項目使用,開箱即用。
注意:
(1)代碼可讀性略差,後續會優化,如有問題,可在下方留言討論。
(2)還未在分佈式環境下進行壓測,如有問題,請提寶貴意見。
(3)分享是一種美德,覺得好的化麻煩點個贊噢!
===================================================================================
package com.xiucai.account.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.regex.Pattern;
/**
* @author lvxiucai
* @description 用戶暱稱工具類(唯一)
* @date 2019/11/2
*/
@Component
@Slf4j
public class NicknameUtil implements CommandLineRunner {
private static ArrayBlockingQueue<Integer> arrayBlockingQueue;
//隊列容量大小
private static final int CAPACITY = 100;
//用戶名字母下標初始值
private static final String INIT_LOOP_VALUE= "A";
//用戶名最大下標初始值
private static final int INIT_VALUE = 0;
//正則校驗規則(用以判斷是否數字)
private static final Pattern pattern = Pattern.compile("[0-9]*");
//redis中用戶名最大下標的key值
private static final String NICKNAME_MAX_IDX ="nickname_max_index";
//redis中用戶名字母【ABC...】的key值
private static final String NICKNAME_LOOP_IDX = "nickname_loop_index";
private static final String NICK_PREFIX = "一水";
private static String LOOP_IDX_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//下一個字母循環的隊列數字所需要達到的上限值
private static int loopCycleSize = 999000;
@Autowired
private RedisUtil redis;
private static RedisUtil redisUtil;
@Override
public void run(String... args) throws Exception {
redisUtil = redis;
//初始化隊列
arrayBlockingQueue = new ArrayBlockingQueue<>(CAPACITY);
//獲取用戶名數字最大下標
String maxIdxStr = redisUtil.get(NICKNAME_MAX_IDX);
//獲取字母下標
String loopIdx = redisUtil.get(NICKNAME_LOOP_IDX);
//初始化redis中暱稱字母下標值
if(StringUtils.isBlank(loopIdx)){
redisUtil.set(NICKNAME_LOOP_IDX,INIT_LOOP_VALUE);
}else if(StringUtils.isNotBlank(loopIdx) && StringUtils.isNotBlank(maxIdxStr)){
Integer maxIdx = Integer.parseInt(maxIdxStr);
//如果達到百萬循環,則進行再次初始化
if(maxIdx >= loopCycleSize){
reInitLoop(loopIdx);
}
}
//初始化redis中的暱稱最大下標值
if(StringUtils.isBlank(maxIdxStr)){
redisUtil.set(NICKNAME_MAX_IDX,INIT_VALUE);
}
//暱稱下標值批量入隊
init();
}
/**
* @author lvxiucai
* @description 獲取用戶名
* @date 2019/11/2
* @return
**/
public static String nextNickname(){
String loopIdx = getNicknameLoopIdxFromRedis();
int nicknameMaxIdx = getNicknameMaxIdxFromRedis();
//如果達到百萬最大下標值,則進行下一個百萬循環
if(nicknameMaxIdx >= loopCycleSize ){
String nextLoopIdx = reInitLoop(loopIdx);
//重新入隊
init();
return NICK_PREFIX+nextLoopIdx+nextIndex();
}
return NICK_PREFIX+loopIdx+nextIndex();
}
/**
* @author lvxiucai
* @description 下一個百萬循環初始化
* @date 2019/11/2
* @param loopIdx 當前的字母
* @return
**/
private static String reInitLoop(String loopIdx){
//獲取下一個百萬循環字母
String nextLoopIdx = LOOP_IDX_STR.charAt(LOOP_IDX_STR.indexOf(loopIdx) + 1)+"";
//更新下一個百萬循環字母到redis中
redisUtil.set(NICKNAME_LOOP_IDX,nextLoopIdx);
//重新初始化redis中的最大下標值,置爲0
redisUtil.set(NICKNAME_MAX_IDX,INIT_VALUE);
log.info("重新下一個百萬循環,nextLoopIdx:{}",nextLoopIdx);
return nextLoopIdx;
}
/**
* @author lvxiucai
* @description 從redis中獲取暱稱字母下標值
* @date 2019/11/2
* @return
**/
private static String getNicknameLoopIdxFromRedis(){
String loopIdx = redisUtil.get(NICKNAME_LOOP_IDX);
if(StringUtils.isNotBlank(loopIdx)){
return loopIdx;
}
redisUtil.set(NICKNAME_LOOP_IDX,INIT_LOOP_VALUE);
loopIdx = INIT_LOOP_VALUE;
return loopIdx;
}
/**
* @author lvxiucai
* @description 從redis中獲取暱稱最大下標
* @date 2019/11/2
* @return
**/
private static Integer getNicknameMaxIdxFromRedis(){
String maxIdxStr = redisUtil.get(NICKNAME_MAX_IDX);
return checkMaxIndex(maxIdxStr);
}
/**
* @author lvxiucai
* @description 獲取暱稱下標
* @date 2019/11/2
* @return
**/
private static Integer nextIndex(){
Integer idx = null;
try {
if(arrayBlockingQueue.size()==0){
init();
}
idx = arrayBlockingQueue.take();
} catch (InterruptedException e) {
log.error("獲取暱稱下標異常:{}",e);
e.printStackTrace();
}
return idx;
}
/**
* @author lvxiucai
* @description 初始化隊列(批量入隊)
* @date 2019/11/2
* @return
**/
private static void init(){
Integer maxIndex = getNicknameMaxIdxFromRedis();
//先更新redis中的暱稱最大下標
String newMaxIndex = redisUtil.getAndSet(NICKNAME_MAX_IDX,maxIndex+CAPACITY);
//啓動很多臺服務並且併發量大的情況下,對數字下標進行比對,保證隊列中數字唯一性
while (maxIndex != null && newMaxIndex != null && !newMaxIndex.equals(maxIndex.toString())){
maxIndex = getNicknameMaxIdxFromRedis();
//先更新redis中的暱稱最大下標
newMaxIndex = redisUtil.getAndSet(NICKNAME_MAX_IDX,maxIndex+CAPACITY);
}
//入隊操作
for(int i=0;i<CAPACITY;i++){
arrayBlockingQueue.offer(i+maxIndex);
}
log.info("暱稱下標初始化完畢,初始值爲:{},大小爲:{}",maxIndex,arrayBlockingQueue.size());
}
/**
* @author lvxiucai
* @description 校驗用戶名下標
* @date 2019/11/2
* @param maxIndexStr
* @return
**/
private static Integer checkMaxIndex(String maxIndexStr){
if(StringUtils.isBlank(maxIndexStr)){
log.error("用戶名下標未初始化!");
throw new RunTimeException("用戶名下標未初始化!");
}
if(!pattern.matcher(maxIndexStr).matches()){
log.error("用戶名下標不能爲字符串!");
throw new RunTimeException("用戶名下標不能爲字符串!");
}
Integer maxIndex = Integer.parseInt(maxIndexStr);
if(maxIndex<0){
log.error("用戶名下標不能爲負數!");
throw new RunTimeException("用戶名下標不能爲負數!");
}
return maxIndex;
}
}