1、實現邏輯
記錄用戶每次的訪問時間,因此對於每個用戶,用列表類型的鍵記錄他最近100次訪問的時間。
如果鍵中的元素超過100個,就判斷時間最早的元素距離現在的時間是否小於1分鍾,如果是,則表示用戶最近1分鍾的訪問次數超過100次,如果不是就將當前時間加入列表中,同時把最早的元素刪除
2、LUA腳本
使用lua腳本實現,保證多個操作的“原子性”
參數說明:
KEYS[1] 傳入表示用戶唯一標識的鍵
ARGV[1] 傳入限制的訪問次數
lua腳本如下:
local len = redis.call('llen',KEYS[1]); redis.replicate_commands(); -- 防止隨機寫入 local now = redis.call('TIME')[1]; -- 當前系統時間,單位是秒 if len < tonumber(ARGV[1]) then redis.call('lpush',KEYS[1],now); return 0; else local lasttime = redis.call('lindex',KEYS[1],-1); -- 取最后一個元素 if now - lasttime < 60 then -- 訪問頻率超過限制 ,這里的60是指60秒 return -1; else -- 訪問頻率未超出限制 redis.call('lpush',KEYS[1],now); redis.call('ltrim',KEYS[1],0,tonumber(ARGV[1])-1); -- 刪除索引在[0,訪問次數-1]以外的元素 return 0; end; end;
list記錄了用戶的訪問時間,list長度為訪問次數。使用此方式進行次數限制,不適用於訪問次數較大的場景,會占用較多內存
通過eval命令執行以上腳本
redis-cli -p 7001 -a 123456 -c --eval "speed.lua" "harara" , 3
speed.lua是lua腳本路徑,"harara"是參數keys ,3是參數argv
注意:參數key和參數argv中間的逗號兩邊都要有空格!!!
如下截圖:限制次數為3次,當在一分鍾之內連續執行3次命令之后,執行第四次返回 -1 , 表示超出訪問次數限制
3、代碼實現(java)
調用controlSpeed方法實現控制訪問次數,返回true表示未超出次數限制
package com.harara.redis.rate; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * 使用redis-list類型 限制用戶1分鍾內訪問次數為100次 * @author : harara * @version : 1.0 * @date : 2021/2/26 9:58 */ @Service @Slf4j public class ListControlSpeed { @Autowired private RedisTemplate redisTemplate; /** * 控速處理 * @param account 用戶賬號 * @param limit 限制次數 * @return true表示未超出限制可繼續推送 false表示超出限制不可繼續推送 */
public boolean controlSpeed(String account,int limit){ String keyParam = "LIMIT:"+account; // 指定 lua 腳本
String luaScript = "local len = redis.call('llen',KEYS[1]);" +
"redis.replicate_commands();" +
"local now = redis.call('time')[1];" +
"if len < tonumber(ARGV[1]) then" +
" redis.call('lpush',KEYS[1],now);" +
" return 0;" +
"else" +
" local lasttime = redis.call('lindex',KEYS[1],-1);" +
" if now - lasttime < 60 then" +
" return -1;" +
" else" +
" redis.call('lpush',KEYS[1],now);" +
" redis.call('ltrim',KEYS[1],0,tonumber(ARGV[1])-1);" +
" return 0;" +
" end;" +
"end;"; try{ // 參數一:redisScript,參數二:key列表,參數三:arg(1、限制條數)
Object result = executeLua(luaScript,Collections.singletonList(keyParam),String.valueOf(limit)); //返回0表示未超過限制的條數 可繼續發送
if((long)result == 0) { return true; } }catch (Exception e){ log.error("對用戶賬號{}進行控速處理出現異常",account,e); return false; } return false; } /** * 執行lua腳本 * @param luaScript lua腳本 * @param keyParams lua腳本中KEYS參數 * @param argvParams lua腳本中ARGV參數 */
public Object executeLua(String luaScript, List<String> keyParams, String... argvParams){ DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript,Long.class); // 參數一:redisScript,參數二:key列表,參數三:arg(可多個) // 注釋掉,spring自帶的執行腳本方法中,集群模式直接拋出不支持執行腳本異常(報錯EvalSha is not supported in cluster environment),只支持單節點不支持集群 //Object result = redisTemplate.execute(redisScript, keyParams,argvParams); //spring自帶的執行腳本方法中,集群模式直接拋出不支持執行腳本異常,此處拿到原redis的connection執行腳本
Object result = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); // 集群模式和單點模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行 // 集群
if (nativeConnection instanceof JedisCluster) { return ((JedisCluster) nativeConnection).eval(luaScript, keyParams, Arrays.asList(argvParams)); } // 單點
else if (nativeConnection instanceof Jedis) { return ((Jedis) nativeConnection).eval(luaScript, keyParams,Arrays.asList(argvParams)); } return null; } }); return result; } }
參考地址:
2、RedisTemplate執行lua腳本,集群模式下報錯解決