当前位置:   article > 正文

限流之redis 有序集合(zset)实现滑动窗口_redistemplate zadd

redistemplate zadd

限流

需求:同一用户1分钟内登录失败次数超过3次,页面添加验证码登录验证,即限流的思想。

常见的限流算法:固定窗口计数器;滑动窗口计数器;漏桶;令牌桶。本篇选择的滑动窗口计数器

redis 有序集合(zset)特性

Redis 有序集合(sorted set)和集合(set)一样都是元素的集合,不允许重复的元素,但不同的是每个元素都会关联一个 double 类型的分数(score)。redis 正是通过分数来为集合中的元素进行从小到大的排序。有序集合的元素是唯一的,但分数(score)可以重复。

源码可对比java的LinkedHashMap和HashMap,都是通过多维护成员变量使无序的集合变成有序的。区别是LinkedHashMap内部是多维护了2个成员变量Entry<K,V> before, after用于双向链表的连接,redis zset是多维护了一个score变量完成顺序的排列。

排序:有序集合中的每个元素都关联着一个分数(score),通过分数来对元素进行排序。这使得有序集合在需要按照特定顺序访问元素的场景中非常有用。

唯一性:有序集合中的每个元素都是唯一的,不会存在重复的元素。

快速插入和删除:有序集合支持快速的插入和删除操作,时间复杂度为 O(log(N)),其中 N 是有序集合中元素的数量。

高效的范围操作:有序集合支持根据分数范围进行查询操作。可以按照分数从小到大或从大到小的顺序,获取指定范围内的元素。

成员访问:可以根据成员(元素)进行访问,例如获取指定成员的分数或排名。

底层实现:有序集合使用跳跃表(Skip List)和哈希表(Hash Table)两种数据结构来实现。跳跃表提供了快速的有序访问能力,而哈希表则提供了快速的成员访问能力。

支持事务和持久化:有序集合和其他 Redis 数据类型一样,可以通过事务(Transaction)和持久化(Persistence)功能来保证数据的一致性和可靠性。

有序集合在很多场景下都非常有用,如排行榜、计数器、范围查找等。它的灵活性和高性能使得 Redis 成为了许多应用程序的首选数据库之一。

滑动窗口算法

滑动窗口算法思想就是记录一个滑动的时间窗口内的操作次数,操作次数超过阈值则进行限流。

java代码实现

key-用户的登录名,value-使用zset。

zset:score-当前登录时间戳,value-也使用当前登录时间戳。

zset的value(元素)是唯一不可重复的,每个元素都有一个关联的分数。如果插入一个已经存在的元素,则会更新该元素的分数。value这里使用时间戳已满足我的需求,但value应该使用可唯一标识数据的值,例如雪花算法生成的唯一标识。

一、RedisTemplate

  1. import java.util.List;
  2. import javax.annotation.Resource;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.data.redis.core.RedisCallback;
  5. import org.springframework.data.redis.core.RedisTemplate;
  6. import org.springframework.data.redis.serializer.RedisSerializer;
  7. import org.springframework.stereotype.Component;
  8. /**
  9. * 滑动窗口计数
  10. *
  11. * @author yangzihe
  12. * @date 2023/8/8
  13. */
  14. @Component
  15. @Slf4j
  16. public class SlidingWindowCounter {
  17. @Resource
  18. private RedisTemplate<String, Object> redisTemplate;
  19. /**
  20. * 数据统计-判断数量是否超过最大限定值
  21. *
  22. * @param key redis key
  23. * @param windowTime 窗口时间,单位:秒
  24. * @param maxNum 最大数量
  25. *
  26. * @return true-超过 false-未超过
  27. */
  28. public boolean countOver(String key, int windowTime, long maxNum) {
  29. // 窗口结束时间
  30. long windowEndTime = System.currentTimeMillis();
  31. // 窗口开始时间
  32. long windowStartTime = windowEndTime - windowTime * 1000L;
  33. // 按score统计key的value中的有效数量
  34. Long count = redisTemplate.opsForZSet().count(key, windowStartTime, windowEndTime);
  35. if (count == null) {
  36. return false;
  37. }
  38. return count > maxNum;
  39. }
  40. /**
  41. * 数据上报-滑动窗口计数增长
  42. *
  43. * @param key redis key
  44. * @param windowTime 窗口时间,单位:秒
  45. */
  46. public void increment(String key, Integer windowTime) {
  47. // 窗口结束时间
  48. long windowEndTime = System.currentTimeMillis();
  49. // 窗口开始时间
  50. long windowStartTime = windowEndTime - windowTime * 1000L;
  51. RedisCallback<List<Object>> pipelineCallback = (connection) -> {
  52. RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
  53. // 在管道中执行多个 ZSet 命令
  54. connection.openPipeline();
  55. // 添加当前时间 value=当前时间戳 score=当前时间戳
  56. connection.zAdd(serializer.serialize(key), windowEndTime, serializer.serialize(String.valueOf(windowEndTime)));
  57. // 清除窗口过期成员
  58. connection.zRemRangeByScore(serializer.serialize(key), 0, windowStartTime);
  59. // 设置key过期时间
  60. connection.expire(serializer.serialize(key), windowTime);
  61. return connection.closePipeline();
  62. };
  63. // 执行管道操作
  64. redisTemplate.executePipelined(pipelineCallback);
  65. }
  66. /**
  67. * 数据统计、数据上报同步处理,判断数量是否超过最大限定值
  68. *
  69. * @param key redis key
  70. * @param windowTime 窗口时间,单位:秒
  71. * @param maxNum 最大数量
  72. *
  73. * @return true-超过 false-未超过
  74. */
  75. public boolean countAndIncrement(String key, int windowTime, long maxNum) {
  76. // 窗口结束时间
  77. long windowEndTime = System.currentTimeMillis();
  78. // 窗口开始时间
  79. long windowStartTime = windowEndTime - windowTime * 1000L;
  80. RedisCallback<List<Object>> pipelineCallback = (connection) -> {
  81. RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
  82. // 在管道中执行多个 ZSet 命令
  83. connection.openPipeline();
  84. // 添加当前时间 value=当前时间戳 score=当前时间戳
  85. connection.zAdd(serializer.serialize(key), windowEndTime, serializer.serialize(String.valueOf(windowEndTime)));
  86. // 按score统计key的value中的有效数量
  87. connection.zCount(serializer.serialize(key), windowStartTime, windowEndTime);
  88. // 清除窗口过期成员
  89. connection.zRemRangeByScore(serializer.serialize(key), 0, windowStartTime);
  90. // 设置key过期时间
  91. connection.expire(serializer.serialize(key), windowTime);
  92. return connection.closePipeline();
  93. };
  94. // 执行管道操作 阻塞api,直到所有命令都被发送并得到结果
  95. List<Object> results = redisTemplate.executePipelined(pipelineCallback);
  96. Long count = (Long) results.get(1);
  97. if (count == null) {
  98. return false;
  99. }
  100. return count > maxNum;
  101. }
  102. }

二、RedissonClient

  1. /**
  2. * 统计请求次数
  3. *
  4. * @param windowTime 窗口时间,单位:秒
  5. * @param key redis key
  6. *
  7. * @return 请求次数
  8. */
  9. public static Integer count(int windowTime, String key) {
  10. // 窗口结束时间
  11. long windowEndTime = System.currentTimeMillis();
  12. // 窗口开始时间
  13. long windowStartTime = windowEndTime - windowTime * 1000L;
  14. try {
  15. // 创建 RBatch 实例,批量执行命令
  16. RBatch batch = redissonClient.createBatch();
  17. // 添加元素 score=当前时间戳 value=请求序列号,唯一不可重复
  18. batch.getScoredSortedSet(key).addAsync(windowEndTime, UUID.randomUUID().toString());
  19. // 统计数据
  20. batch.getScoredSortedSet(key).countAsync(windowStartTime, true, windowEndTime, true);
  21. // 清除窗口过期成员
  22. batch.getScoredSortedSet(key).removeRangeByScoreAsync(0, true, windowStartTime, false);
  23. // 设置key过期时间
  24. batch.getScoredSortedSet(key).expireAsync(Duration.ofSeconds(windowTime));
  25. // 执行管道命令
  26. BatchResult<?> batchResult = batch.execute();
  27. // 返回统计数量
  28. List<?> responses = batchResult.getResponses();
  29. return (Integer) responses.get(1);
  30. } catch (Exception e) {
  31. log.error("统计请求次数异常!", e);
  32. return null;
  33. }
  34. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/276916
推荐阅读
相关标签
  

闽ICP备14008679号