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;
    }


}
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐