Redis+Lua双剑合一 制服接口限流

591 阅读1分钟

背景:在对外的接口中,为了限制每个用户的接口访问频率,或者是付费接口(多少钱多少次调用次数等),作为最苦逼的代码搬运工,我们需要掏出“代码大全”,话不多说,搞起

  • 为啥要用Lua? 因为Redis执行Lua脚步是原子性的,所以我们可以在脚步中写我们的判断逻辑

  • 先上一段Lua

//从redis读取key为KEYS[1]的值
local val = redis.call("GET", KEYS[1])
//如果不存在,设置key,value 过期时间
if not val 
then
    redis.call('SET', KEYS[1], ARGV[1])
    redis.call('EXPIRE', KEYS[1], ARGV[2])
    return true
//如果值小于阈值,则计数加一
elseif val < ARGV[3]
then
    redis.call('INCR', KEYS[1])
    return true
//超过阈值,返回false
else
    return false
end

KEYS:表示传入的key数组,下标从1开始

ARGV:表示传入的参数

  • 上代码
  1. xml 配置redis依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>
    </dependency>
</dependencies>
  1. conf 配置redisTemplate
@Configuration
class RedisConf {
    @Bean
    fun redisTemplate(redisConnectionFactory: LettuceConnectionFactory): RedisTemplate<String, Serializable> {
        val template = RedisTemplate<String, Serializable>();
        template.keySerializer = StringRedisSerializer();
        template.valueSerializer = GenericJackson2JsonRedisSerializer();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}
  1. redis+lua 调用lua脚本
@RestController
@RequestMapping("/test")
class TestController {
    @Autowired
    private lateinit var redisTemplate: RedisTemplate<String, Serializable>

    @GetMapping("/1/{userId}")
    fun test(@PathVariable userId: String): Boolean {
        val luaScript = buildScript()

        val redisScript: RedisScript<Boolean> = DefaultRedisScript(luaScript, Boolean::class.java)
        return redisTemplate.execute(redisScript, listOf("test:uri:$userId"), 1, 60, 5)
    }

    private fun buildScript(): String {
        return """
            local key = KEYS[1]
            local val = redis.call("GET", key);
            if not val 
            then
                redis.call('SET', KEYS[1], ARGV[1])
                redis.call('EXPIRE', KEYS[1], ARGV[2])
                return true
            elseif val < ARGV[3]
            then
                redis.call('INCR', KEYS[1])
                return true
            else
                return false
            end
        """.trimIndent()
    }
}