利用redis作次數限制的小結

有一些須要限制次數的場景,好比api調用次數限制、在一段時間內只能使用幾回的限制,在幾秒內、幾分鐘時間內只能使用幾回的限制。簡單的實現能夠把須要作限制的次數放在redis中,利用redis的特色進行限制。這裏只是對本人的一些用法作個簡單的小結。java

1.在單位時間內只能使用N次的限制

常見於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("超過限制");
// }

2.在最近單位時間內只能使用N次的限制

舉個例子,最近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

相關文章
相關標籤/搜索