之前項(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;
}
}