Bootstrap

Redis与Lua高效实现Redis批量处理与限流


引言

Redis 作为目前最主流的分布式缓存数据库,在高并发环境下表现出色,并支持多种数据类型。为了进一步提高效率和减少延迟,Redis 支持直接在服务端执行 Lua 脚本的能力,这使得我们可以更灵活地控制数据处理流程而无需频繁地在网络间传输命令。

本文将详细介绍如何在SpringBoot中使用Lua脚本来操作Redis缓存,从而实现批量操作和限流等功能。


为什么是Lua

Redis本身不具备批量操作的命令以及不保证事务。当我们的KV值很大时,在实际使用中大Key的出现会导致Redis的IO变慢从而导致服务不可处理速度变慢甚至不可用。此时我们可以对KV进行拆分并批量操作。

使用Lua操作Redis的好处:

  1. 原子性
    Lua 脚本在 Redis 中执行时,整个脚本被视为单一命令处理。这意味着一旦脚本开始执行,它就会被完全执行完毕,中间不会被打断或插入其他命令。这种特性非常适合于需要保持一致性的批量操作。

  2. 减少网络往返次数
    Redis本身并不支持批量操作。相比之下,使用 Lua 脚本可以在一次请求中包含所有必要的操作,显著降低了网络通信的成本,这对于高并发环境尤其有利。

  3. 灵活性
    Lua 是一种轻量级且易于嵌入的语言。Redis 允许在其内部环境中执行 Lua 代码,这让开发者能够编写复杂逻辑而不需要在客户端和服务端之间来回传递数据或命令。这样不仅简化了开发过程还增强了系统的可维护性。

  4. 安全性
    由于所有逻辑都在 Redis 内部完成而不是暴露给外部执行环境,因此可以更好地保护敏感数据不被泄露。此外,通过合理设计脚本还可以有效地防止某些类型的注入攻击或其他安全威胁。

  5. 性能优化
    对于需要实时响应的应用来说,减少延迟至关重要。通过将计算密集型任务放在靠近数据的地方(即在 Redis 服务器上),可以大大缩短处理时间并提高整体性能表现。

实战

引入Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml配置Redis连接

spring:
  # redis 配置
  redis:
    host: 127.0.0.1 # 地址
    port: 6379 # 端口
    password: password # 密码,建议生产环境开启
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 5
        # 连接池的最大数据库连接数
        max-active: 5
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

我们采用springboot的redis-starter来操作Redis,完成上述配置后即可通过直接注入RedisTemplate来操作Redis了。

Redis工具类


@Service
public class RedisService {

    /**
     * 限流脚本
     */
    private final String LIMIT_SCRIPT = "local key = KEYS[1]\n" +
            "local time = tonumber(ARGV[1])\n" +
            "local count = tonumber(ARGV[2])\n" +
            "local current = redis.call('get', key);\n" +
            "if current and tonumber(current) > count then\n" +
            "    return tonumber(current);\n" +
            "end\n" +
            "current = redis.call('incr', key)\n" +
            "if tonumber(current) == 1 then\n" +
            "    redis.call('expire', key, time)\n" +
            "end\n" +
            "return tonumber(current);";

    /**
     * 批量查询脚本
     */
    private final String BATCH_SCRIPT = "local result = {} " +
            "for _, key in ipairs(KEYS) do " +
            "   local value = redis.call('get', key) " +
            "   table.insert(result, value) " +
            "end " +
            "return result";

    @Autowired
    private RedisTemplate redisTemplate;

    public void multiSet(Map<String, String> map) {
        redisTemplate.opsForValue().multiSet(map);
    }

    public List multiGet(List<String> keys) {
        DefaultRedisScript<List> redisScript = new DefaultRedisScript<>(BATCH_SCRIPT, List.class);
        // 执行Lua脚本
        List values = (List) redisTemplate.execute(redisScript, keys);
        return values;
    }

    /**
     * 删除缓存对象
     * @param collection 多个对象
     */
    public long delete(Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 限流
     *
     * @param key 缓存键
     * @param time 限流时间,单位秒
     * @param count 限流次数
     */
    public boolean limit(String key, int time, int count) {
        DefaultRedisScript<Long> limitScript = new DefaultRedisScript<>(LIMIT_SCRIPT, Long.class);
        // 当前请求次数
        Long currentRequestCount = (Long) redisTemplate.execute(limitScript, Arrays.asList(key), time, count);
        return currentRequestCount > count;
    }

}

使用示例:

@Service
public class TestService {

    @Autowired
    private RedisService2 redisService;

    /**
     * 批量操作
     */
    public void batchOperate() {
        Map<String, String> map = Map.of("key1", "value1", "key2", "value2");

        // 插入多个
        redisService.multiSet(map);

        //获取
        List list = redisService.multiGet(List.of("key1", "key2"));
        System.out.println(list);
    }

    /**
     * 依据key,在指定时间内限流
     * @param limitKey
     * @return
     */
    public boolean limit(String limitKey) {
        return redisService.limit(limitKey, 10, 10);
    }
}

除此之外,Redis结合Lua还可以实现更多功能,常见的有分布式锁、秒杀等,只需要在脚本上做修改就行。能够很好的保证这些场景下的性能和数据一致性问题。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;