有一些須要限制次數的場景,好比api調用次數限制、在一段時間內只能使用幾回的限制,在幾秒內、幾分鐘時間內只能使用幾回的限制。簡單的實現能夠把須要作限制的次數放在redis中,利用redis的特色進行限制。這裏只是對本人的一些用法作個簡單的小結。java
常見於api調用次數限制,時間能夠是1秒、1分鐘、1小時,1天。其餘規則的時間限制,須要自定義。這種的用法相對簡單,直接用incr方法就能夠實現。這裏有個小細節,是先用get方法來獲取key的值判斷是否達到上限,仍是直接用incr的返回值?這裏我選擇直接獲取incr的值,由於先作get判斷,以後再作incr操做,若是遇到併發,可能會形成髒讀,固然也能夠放在事務中實現。redis
key的構造是一個前綴+對應的時間格式。好比要求是每秒的限制,時間格式就設置爲yyyyMMddHHmmss,若是是每1分鐘的限制,時間格式就設置爲yyyyMMddHHmm。若是是每5秒的限制呢?相似每秒的限制,可是設置時間點的時候,須要再計算,本身定義一個規則,好比取0秒、5秒、10秒,[0,5)秒取0,[5,10)取5,能夠[0,5)秒取2,[5,10)取7,總之就是定義規則,判斷時間點對應的區間,取區間的表明值,構造最後的key。最後等待key失效的策略清理過時的key。api
/** * * @param key * @param limitSeconds key有效期 * @param limitTimes 限制次數 * @return -1表示超過限制 */ public Long incr(final String key, final int limitSeconds, final int limitTimes) { if(StringUtils.isEmpty(key)) { return 0L; } Long ret = 1L; if (!redisTemplate.hasKey(key)) { redisTemplate.opsForValue().increment(key, 1); redisTemplate.expire(key, limitSeconds, TimeUnit.SECONDS); return ret; } ret = redisTemplate.opsForValue().increment(key, 1); return ret > limitTimes ? -1L : ret; } //調用,省略各類設置,每秒10次的限制 // RedisTest test = new RedisTest(); // if(test.incr("test_20171203170252", 5, 10) < 0){ // key 設置5秒過時 // System.out.println("超過限制"); // }
舉個例子,最近1分鐘內要求限制N次。數據結構
關於這個需求,腦子中第一種想到的方法就是利用keys 操做來實現。首先是構造key,直接一個前綴+時間戳,對應的值設置過時時間爲1分鐘,這樣keys 前綴就能夠獲得1分鐘內有效的個數。這種作法很簡單,可是效率不高,若是遇到redis集羣的狀況,效率更低。曾經遇到過一次由於併發高了,用keys 獲取數據致使cpu 100%的狀況。併發
public Long incr2(final String key, final int limitSeconds, final int limitTimes) { if(StringUtils.isEmpty(key)) { return 0L; } long ret = -1L; if ((ret = redisTemplate.keys(key + "*").size()) >= limitTimes) { return -1L; } final String k = key + System.currentTimeMillis(); redisTemplate.execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection redisConnection) throws DataAccessException { redisConnection.setEx(k.getBytes(), limitSeconds, "1".getBytes()); return null; } }); return ret + 1; }
以後在實際項目的測試環境中,redis使用了cluster,估計是環境的什麼配置有問題,用keys 操做的時候,得不到想要的結果。想到redis中還有list這種數據結構,有種預感,應該能夠用list實現這種限制,因而看了api,發現llen 還有lrange,忽然腦洞一開,想到能夠利用這幾個命令來實現。經過lpush,把最久的數據放在最右邊,經過lrange獲取前N個數據,經過ltrim刪除過時的數據,因而用list來實現的限制就完成了。沒有對應的key時,lpush 而且設置過時時間。設置新的值,再更新一下key的有效期。代碼中沒作事務,就是簡單的實現,有須要再簡單加個事務實現就行。測試
public Long incr3(final String key, final int limitSeconds, final int limitTimes) { if(StringUtils.isEmpty(key)) { return 0L; } long ret = -1L; long time = System.currentTimeMillis(); final String timestamp = "" + time; if (!redisTemplate.hasKey(key)) { redisTemplate.execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection redisConnection) throws DataAccessException { redisConnection.lPush(key.getBytes(), timestamp.getBytes()); redisConnection.expire(key.getBytes(), limitSeconds); return null; } }); return 1L; } List<String> list = redisTemplate.opsForList().range(key, 0, limitTimes); int t = 0; // 倒序遍歷,查找最後一個沒過時的下標 for (int i = list.size() - 1; i >= 0; i--) { if (Long.parseLong(list.get(i)) > time - limitSeconds * 1000) { t = i; break; } } // 清除過時的list值 redisTemplate.opsForList().trim(key, 0, t); if (t + 1 < limitTimes) { redisTemplate.execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection redisConnection) throws DataAccessException { redisConnection.lPush(key.getBytes(), timestamp.getBytes()); redisConnection.expire(key.getBytes(), limitSeconds); return null; } }); ret = t + 2; } else { ret = -1L; } return ret; }
以上就是一些我的的用法總結,遇到其餘的再繼續完善。spa