Redis分布式鎖注解方式生產(chǎn)實(shí)踐

之前項(xiàng)目中涉及到一個(gè)場(chǎng)景:

用戶每天可以在app上簽到領(lǐng)取金幣,金幣到達(dá)一定數(shù)量后可以換成人民幣

場(chǎng)景很簡(jiǎn)單,其實(shí)仔細(xì)想下涉及到要考慮的細(xì)節(jié)還是很多,這里不一一列舉,主要說(shuō)一下,如果用戶通過(guò)fiddler插件抓取到領(lǐng)取金幣的接口請(qǐng)求,然后寫(xiě)一個(gè)腳本循環(huán)去調(diào)用,那么是不是就賺大發(fā)了。也許有的人會(huì)在心里想,誰(shuí)會(huì)為了那點(diǎn)金幣還是去安裝各種抓包工具,然后又寫(xiě)腳本去循環(huán)調(diào)用接口。。。其實(shí)可能你沒(méi)遇到過(guò)而已,這種情況真的是非常多。所以作為程序員,我們開(kāi)發(fā)這個(gè)功能,如果不能保證和杜絕這種刷金幣的情況,那么會(huì)給公司帶來(lái)很大的損失,后果肯定也會(huì)非常嚴(yán)重。

怎么突然感覺(jué)到,程序員這條路也真的不容易走,一不小心一些細(xì)節(jié)沒(méi)有考慮到,開(kāi)發(fā)的功能模塊有漏洞給公司造成損失,瞬間就玩完了。

下面把自己之前實(shí)踐過(guò)的Redis分布式鎖以注解的方式調(diào)用,非常小的侵入性,簡(jiǎn)單一個(gè)注解就可以搞定重復(fù)刷單請(qǐng)求。下面的代碼經(jīng)過(guò)生存環(huán)境的考驗(yàn),而且項(xiàng)目上線后,沒(méi)有出現(xiàn)過(guò)重復(fù)刷單請(qǐng)求。

一、首先是開(kāi)發(fā)限制用戶重復(fù)提交的注解,在其他需要進(jìn)行限制的方法上,直接使用@RedisLimitLock注解即可
/**
 * @Description Redis鎖限制用戶重復(fù)提交 Annotation
 * @Version 1.0
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLimitLock {
    String value() default "";
}
二、開(kāi)發(fā)限制重復(fù)提交的切面
/**
 * @Description redis鎖限制重復(fù)提交 切面
 **/
@Aspect
@Component
public class RedisLimitLockAspect {

    private static final String TOKEN = "token"; //parameter  token
    private static final int TIMEOUT = 3; //超時(shí)時(shí)間  單位:秒
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLimitLockAspect.class);

    //記錄當(dāng)前線程標(biāo)志 ,使用@Before @After時(shí)保存當(dāng)前線程的標(biāo)志
    private static final ThreadLocal<String> requestUUID = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLockKey = new ThreadLocal<>();

    /**
     * 定義切點(diǎn)
     */
    @Pointcut("@annotation(com.hstrivl.aspect.annotation.RedisLimitLock)")
    public void controllerAspect(){}

    /**
     * 環(huán)繞通知,根據(jù)條件控制目標(biāo)方法是否執(zhí)行
     * @param proceedingJoinPoint
     */
    @Around("controllerAspect()")
    public void doAround(ProceedingJoinPoint proceedingJoinPoint){
        final Object[] parameterValues = proceedingJoinPoint.getArgs(); //切入方法參數(shù)值集合
        CodeSignature codeSignature = (CodeSignature) proceedingJoinPoint.getStaticPart().getSignature();
        String[] parameterNames = codeSignature.getParameterNames(); //切入方法參數(shù)名集合parameterName <--> parameterValue
        String methodName = proceedingJoinPoint.getSignature().getName();//切入方法名稱
        String paramToken = "";
        for(int i = 0, length = parameterNames.length; i < length; i++) {
            if (TOKEN.equalsIgnoreCase(parameterNames[i])) {
                paramToken = (String) parameterValues[i];
                break;
            }
        }
        //如果通過(guò)切點(diǎn)方式?jīng)]有取到參數(shù),通過(guò)request取
        if (StringUtils.isEmpty(paramToken)) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            paramToken = request.getParameter(TOKEN);
        }
        String requestId = UUID.randomUUID().toString();
        String lockKey = paramToken + methodName; //同一個(gè)用戶并發(fā)操作
        LOGGER.info("lockKey:{}", lockKey);
        //String lockKey = UUID.randomUUID().toString() + methodName; //模擬不同用戶同時(shí)操作
        if (!RedisUtil.tryGetDistributedLock(lockKey, requestId, TIMEOUT)) {
            //沒(méi)有獲取到鎖
            LOGGER.info("get redis lock failed! paramerNames:{},parameterValues:{}", Arrays.toString(parameterNames),Arrays.toString(parameterValues));
        } else {
            LOGGER.info("get redis lock success");
            try {
                proceedingJoinPoint.proceed(); //執(zhí)行目標(biāo)方法
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                //執(zhí)行完之后釋放鎖
                RedisUtil.releaseDistributedLock(lockKey, requestId);
            }
        }
    }

    /**
     * 前置通知 目標(biāo)方法執(zhí)行前的一些操作**按需添加**
     * @param joinPoint 切點(diǎn)
     */
    //@Before("controllerAspect()")
    public void doBefore(JoinPoint joinPoint) {
        final Object[] parameterValues = joinPoint.getArgs(); //切入方法參數(shù)值集合
        CodeSignature codeSignature = (CodeSignature) joinPoint.getStaticPart().getSignature();
        String[] parameterNames = codeSignature.getParameterNames(); //切入方法參數(shù)名集合parameterName <--> parameterValue
        String methodName = joinPoint.getSignature().getName();//切入方法名稱
        String paramToken = "";
        for(int i = 0, length = parameterNames.length; i < length; i++) {
            if (TOKEN.equalsIgnoreCase(parameterNames[i])) {
                paramToken = (String) parameterValues[i];
                break;
            }
        }
        String requestId = UUID.randomUUID().toString();
        requestUUID.set(requestId);
        String lockKey = paramToken + methodName; //同一個(gè)用戶并發(fā)操作
        //String lockKey = UUID.randomUUID().toString() + methodName; //不同用戶同時(shí)操作
        threadLockKey.set(lockKey);
        LOGGER.info("thread before-" + Thread.currentThread().getName());
        if (!RedisUtil.tryGetDistributedLock(lockKey, requestId, TIMEOUT)) {
            //沒(méi)有獲取到鎖
            LOGGER.info("get redis lock failed!");
            return;
        } else {
            LOGGER.info("get redis lock success");
        }
    }

    /**
     * 后置通知 目標(biāo)方法執(zhí)行后的一些操作**按需添加**
     * @param
     */
    //@After("controllerAspect()")
    public void doAfter(JoinPoint joinPoint) {
        String requestId = null;
        String lockKey = null;
        try {
            requestId = requestUUID.get();
            lockKey = threadLockKey.get();
        } finally {
            requestUUID.remove();
            threadLockKey.remove();
            if (!RedisUtil.releaseDistributedLock(lockKey, requestId)) {
                LOGGER.info("release redis lock failed!");
            }else{
                LOGGER.info("release redis success");
            }
        }
    }

    /**
     * 異常通知 目標(biāo)方法異常時(shí)的操作
     * @param e
     */
    @AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
    public void afterThrowing(JoinPoint point,Exception e) {
        String requestId = null;
        String lockKey = null;
        try {
            requestId = requestUUID.get();
            lockKey = threadLockKey.get();
        } finally {
            requestUUID.remove();
            threadLockKey.remove();
            if (!StringUtils.isEmpty(lockKey) && !StringUtils.isEmpty(requestId)) {
                RedisUtil.releaseDistributedLock(lockKey, requestId);//釋放redislock
            }
        }

    }
}
三、在對(duì)應(yīng)Controller里method上添加限制重復(fù)提交的注解@RedisLimitLock
   /**
     * 每天簽到
     * @param response
     * @param request
     * @param token
     * @throws IOException
     */
    @RedisLimitLock
    @RequestMapping(value = "/userNormalSign.html", method = RequestMethod.POST)
    public void userNormalSign(HttpServletResponse response, HttpServletRequest request, String token) throws IOException {
        // 業(yè)務(wù)邏輯代碼
    }
四、RedisUtil及JedisUtil工具類(lèi)提供給大家參考:
/**
  * RedisUtil
  */
public class RedisUtil {
    /**
     * @Description: 把一個(gè)對(duì)象存入redis
     */
    public static void setObjectValue(String key, Object value) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        jedis.set(key.getBytes(), su.serialize(value));
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 把一個(gè)字符串存入redis
     */
    public static void setStringValue(String key, String value) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.set(key, value);
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 判斷key是否存在
     */
    public static boolean isHaveRedisKey(String key) {
        Jedis jedis = JedisUtil.getJedis();
        boolean flag = jedis.exists(key);
        JedisUtil.returnResource(jedis);
        return flag;
    }

    /**
     * @Description: 把一個(gè)對(duì)象存入redis
     */
    public static void setExpireObject(String key, int time, Object value) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        jedis.setex(key.getBytes(), time, su.serialize(value));
        JedisUtil.returnResource(jedis);
    }

    public static void setExpireString(String key, int time, String value) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.setex(key, time, value);
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 查詢一個(gè)key從redis
     */
    public static Object getObjectValue(String key) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        Object o = su.unserialize(jedis.get(key.getBytes()));
        JedisUtil.returnResource(jedis);
        return o;
    }

    /**
     * @Description: 查詢一個(gè)key從redis
     */
    public static String getStringValue(String key) {
        Jedis jedis = JedisUtil.getJedis();
        String value = jedis.get(key);
        JedisUtil.returnResource(jedis);
        return value;
    }

    /**
     * @Description: 刪除對(duì)象
     */
    public static void removeString(String key) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.del(key);
        JedisUtil.returnResource(jedis);
    }

    public static void removeObject(String key) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.del(key.getBytes());
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 把一個(gè)對(duì)象存入隊(duì)列
     */
    public static void setQueueValue(String key, Object value) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        jedis.rpush(key.getBytes(), su.serialize(value));
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 把一個(gè)對(duì)象取出隊(duì)列
     */
    public static Object getQueueValue(String key) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        Object o = su.unserialize(jedis.lpop(key.getBytes()));
        JedisUtil.returnResource(jedis);
        return o;
    }

    /**
     * @Description: 設(shè)置key的過(guò)期時(shí)間
     */
    public static void setExpireKey(String key, int seconds) {

        Jedis jedis = JedisUtil.getJedis();
        jedis.expire(key, seconds);
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 把一個(gè)字符串存入redis
     */
    public static void setAppendValue(String key, int time, String value) {
        Jedis jedis = JedisUtil.getJedis();
        if (jedis.get(key) != null) {
            jedis.append(key, value);
        } else {
            jedis.setex(key, time, value);
        }
        JedisUtil.returnResource(jedis);
    }

    /**
     * @Description: 查詢key的過(guò)期時(shí)間
     */
    public static int getKeyExpire(String key) {
        Jedis jedis = JedisUtil.getJedis();
        long time = jedis.pttl(key);
        JedisUtil.returnResource(jedis);
        return Integer.parseInt(time / 1000 + "");
    }

    /**
     * @Description: 自動(dòng)增加值
     */
    public static void incrementKey(String key, Integer increment) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.incrBy(key, increment);
        JedisUtil.returnResource(jedis);
    }

    /**
     * 批量添加String val
     */
    public static void batchStringVal(Map<String, String> map) {
        Jedis jedis = JedisUtil.getJedis();
        Pipeline pipeline = jedis.pipelined();
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        Iterator<Map.Entry<String, String>> it = entrySet.iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            pipeline.set(entry.getKey(), entry.getValue());
        }
        pipeline.sync();
        JedisUtil.returnResource(jedis);
    }

    /**
     * 批量添加Hash String
     */
    public static void batchHashString(String key, Map<String, String> map) {
        Jedis jedis = JedisUtil.getJedis();
        Pipeline pipeline = jedis.pipelined();
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        Iterator<Map.Entry<String, String>> it = entrySet.iterator();
        while (it.hasNext()) {
            Map.Entry<String, String> entry = it.next();
            pipeline.hset(key, entry.getKey(), entry.getValue());
        }
        pipeline.sync();
        JedisUtil.returnResource(jedis);
    }


    /**
     * 批量添加Hash Object
     */
    public static void batchHashObject(String key, Map<String, Object> map) {
        Jedis jedis = JedisUtil.getJedis();
        Pipeline pipeline = jedis.pipelined();
        Set<Map.Entry<String, Object>> entrySet = map.entrySet();
        SerializeUtil su = new SerializeUtil();
        Iterator<Map.Entry<String, Object>> it = entrySet.iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> entry = it.next();
            pipeline.hset(key.getBytes(), entry.getKey().getBytes(), su.serialize(entry.getValue()));
        }
        pipeline.sync();
        JedisUtil.returnResource(jedis);
    }

    /**
     * Hash get
     */
    public static String hGetString(String key, String field) {
        Jedis jedis = JedisUtil.getJedis();
        String val = jedis.hget(key, field);
        JedisUtil.returnResource(jedis);
        return val;
    }

    /**
     * Hash get
     */
    public static Object hGetObject(String key, String field) {
        Jedis jedis = JedisUtil.getJedis();
        SerializeUtil su = new SerializeUtil();
        Object val = su.unserialize(jedis.hget(key.getBytes(), field.getBytes()));
        JedisUtil.returnResource(jedis);
        return val;
    }

    /**
     * 批量查詢
     */
    public static List getQueryValues(List<String> list) {
        List<String> values = new ArrayList<>();
        for (String key : list) {
            String o = getStringValue(key);
            values.add(o);
        }
        return values;
    }

    /**Redis distribute lock*/
    private static final String LOCK_SUCCESS = "OK";  //成功獲取鎖標(biāo)識(shí)
    private static final String SET_IF_NOT_EXIST = "NX"; //不存在時(shí)NX  存在時(shí)XX
    private static final String SET_WITH_EXPIRE_TIME = "EX"; //超時(shí)設(shè)置 EX-秒 PX-毫秒
    private static final Long RELEASE_SUCCESS = 1L;  //鎖釋放成功標(biāo)志

    /**
     * 嘗試獲取分布式鎖
     * @param lockKey 鎖
     * @param requestId 請(qǐng)求標(biāo)識(shí)
     * @param expireTime 超期時(shí)間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
        Jedis jedis = JedisUtil.getJedis();
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        JedisUtil.returnResource(jedis);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 釋放分布式鎖
     * @param lockKey 鎖
     * @param requestId 請(qǐng)求標(biāo)識(shí)
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(String lockKey, String requestId) {
        Jedis jedis = JedisUtil.getJedis();
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        JedisUtil.returnResource(jedis);
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 根據(jù)key 獲取map
     * @param key
     * @return
     */
    public static Map<String, String> getMapByKey(String key) {
        Jedis jedis = JedisUtil.getJedis();
        Map<String,String> result = jedis.hgetAll(key);
        JedisUtil.returnResource(jedis);
        return result == null ? new HashMap<String, String>() : result;
    }

    /**
     * userId:Map<taskId,awardCount>
     * 自然日過(guò)期
     * @param key
     * @param map
     */
    public static void setMapValueWithExpire(String key, Map<String, String> map) {
        Jedis jedis = JedisUtil.getJedis();
        jedis.hmset(key, map);
        jedis.expire(key,getExpireTimeOfDay());
        JedisUtil.returnResource(jedis);
    }

    /**
     * 獲取一個(gè)自然日的過(guò)期時(shí)間(當(dāng)天的23:59:59減去當(dāng)前時(shí)間)  單位:秒
     * @return
     */
    private static int getExpireTimeOfDay(){
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date();
        String str = format.format(date);
        Date date2 = null;
        try {
            date2 = format.parse(str);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        long dayMis = 1000 * 60 * 60 * 24;//一天的毫秒
        long curMillisecond = date2.getTime();
        long resultMis = curMillisecond + (dayMis - 1); //當(dāng)天最后一秒
        long nowTimeMills = System.currentTimeMillis();
        int result = (int) ((resultMis - nowTimeMills) / 1000);//轉(zhuǎn)換成秒
        return result;
    }
}

JedisUtil工具類(lèi):

/**
 * JedisUtil 工具類(lèi)
 */
public class JedisUtil {


    //Redis服務(wù)器IP  
    private static String server_ip = PropertiesHelper.getProperty("server_ip", "redis.properties");

    //Redis的端口號(hào)  
    private static String port = PropertiesHelper.getProperty("port", "redis.properties");

    //訪問(wèn)密碼  
    private static String auth = PropertiesHelper.getProperty("auth", "redis.properties");

    private static JedisPool pool = null;

    /**
     * 構(gòu)建redis連接池
     *
     * @param ip
     * @param port
     * @return JedisPool
     */
    public static JedisPool getPool() {
        if (pool == null) {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setTestOnBorrow(false);
            config.setTestWhileIdle(true);
            config.setMaxIdle(10);//the max number of free
            config.setMaxTotal(10000);
            pool = new JedisPool(config, server_ip, Integer.parseInt(port));
        }
        return pool;
    }

    /**
     * 返還到連接池
     *
     * @param pool
     * @param redis
     */
    public static void returnResource(Jedis redis) {
        if (redis != null) {
            redis.close();
        }
    }

    /**
     * 獲取數(shù)據(jù)
     */
    public static Jedis getJedis() {
        Jedis j = null;
        try {
            j = getPool().getResource();
            if (!"".equals(auth) && auth != null) {
                j.auth(auth);
            }
        } catch (Exception e) {
            try {
                JedisPoolConfig config = new JedisPoolConfig();
                config.setTestOnBorrow(false);
                config.setTestWhileIdle(true);
                config.setMaxIdle(10);//the max number of free
                config.setMaxTotal(10000);
                pool = new JedisPool(config, server_ip, Integer.parseInt(port));
                j = pool.getResource();
                j.auth(auth);
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        return j;
    }
}

配置文件獲取工具類(lèi):

/**
 * properties工具類(lèi)
 */
public class PropertiesHelper {

    public static String getProperty(String name, String properties) {
        String result = "";
        Resource resource = new ClassPathResource(properties);
        Properties props = null;
        try {
            props = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException e) {
            System.out.println("讀取配置文件" + properties + "失敗,原因:配置文件不存在!");
        }
        for (String key : props.stringPropertyNames()) {
            if (name.equals(key)) {
                result = props.getProperty(key);
            }
        }
        return result;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容