redis实现计数器功能
1. redisTemplateService.incrBy(key, 1L);
功能:
incrBy
是 Redis 中的一个命令,用于将存储在指定键的数值增加指定的增量值。如果键不存在,则在执行操作之前将其初始化为0。- 在这个代码片段中,
incrBy
方法被调用,传入了两个参数:key
: 这是要操作的键。1L
: 这是要增加的值,这里是一个长整型(long)值,值为1。
用途:
- 这个操作通常用于计数器场景。例如,统计某个事件的发生次数、用户访问次数等。每次调用这个方法,都会将对应键的值增加1。
2. redisTemplateService.expire(key, 24, TimeUnit.HOURS);
功能:
expire
是 Redis 中的一个命令,用于设置键的过期时间。当键的过期时间到达时,键会自动被删除。- 在这个代码片段中,
expire
方法被调用,传入了三个参数:key
: 这是要设置过期时间的键。24
: 这是过期时间的长度,这里是24。TimeUnit.HOURS
: 这是时间单位,表示小时。
用途:
- 这个操作通常用于控制数据的生命周期。例如,可以设置一个缓存项在24小时后自动失效,从而避免数据长期占用内存。
综合解释:
redisTemplateService.incrBy(key, 1L);
redisTemplateService.expire(key, 24, TimeUnit.HOURS);
这段代码首先将指定键 key
对应的值增加1,然后将该键设置为24小时后过期。这通常用于需要对某个事件进行计数并希望在一定时间内自动清除计数的场景。例如,可以用在网站访问计数器中,每访问一次页面就将计数器加1,并且希望计数器在24小时后自动重置。
示例应用场景:
假设我们有一个网站,我们希望记录每天的访问量,并在每天结束时重置计数。我们可以使用上述代码来实现这一需求:
String key = "daily_visits";
redisTemplateService.incrBy(key, 1L); // 增加访问计数
redisTemplateService.expire(key, 24, TimeUnit.HOURS); // 设置24小时后过期
这样,每天的访问量都会被记录到 daily_visits
键中,并且在24小时后自动重置为0。
seqNo同一天内会获取到相同值的场景
seqNo
是通过 redisTemplateService.incrBy(key, 1L)
获取的。这个操作会将指定键 key
对应的值增加1,并返回增加后的值。
出现相同值可能的情况:
-
并发请求:
- 如果多个线程或进程几乎同时执行这段代码,它们可能会读取到相同的初始值,然后各自增加1,导致最终
seqNo
相同。例如,两个线程都读取到初始值为100,然后各自增加1,结果都是101。
- 如果多个线程或进程几乎同时执行这段代码,它们可能会读取到相同的初始值,然后各自增加1,导致最终
-
Redis 集群环境:
- 在 Redis 集群环境中,如果多个节点同时处理对同一个键的
incrBy
操作,并且这些操作没有同步机制,也可能导致相同的seqNo
。
- 在 Redis 集群环境中,如果多个节点同时处理对同一个键的
-
网络延迟和重试机制:
- 在某些情况下,由于网络延迟或其他原因,客户端可能会重试操作。如果重试发生在第一次操作完成之前,那么第二次操作也会基于相同的初始值进行增加,从而导致相同的
seqNo
。
- 在某些情况下,由于网络延迟或其他原因,客户端可能会重试操作。如果重试发生在第一次操作完成之前,那么第二次操作也会基于相同的初始值进行增加,从而导致相同的
-
应用重启或故障恢复:
- 如果应用程序在一天内多次重启或发生故障恢复,而没有持久化计数器的状态,那么每次重启后计数器都会从0开始,导致在短时间内出现相同的
seqNo
。
- 如果应用程序在一天内多次重启或发生故障恢复,而没有持久化计数器的状态,那么每次重启后计数器都会从0开始,导致在短时间内出现相同的
解决方案:
为了避免上述情况导致的 seqNo
重复,可以考虑以下几种方法:
-
使用分布式锁:
- 在执行
incrBy
操作时,使用分布式锁来确保同一时间只有一个线程能够修改该键的值。这样可以防止并发问题。
- 在执行
-
持久化计数器状态:
- 将计数器的当前值持久化到数据库或其他持久化存储中,以便在应用重启后能够恢复计数器的状态。
-
使用唯一标识符:
- 结合业务逻辑,使用其他唯一标识符(如用户ID、订单ID等)来生成唯一的序列号,而不是依赖简单的自增操作。
-
Redis Lua脚本:
- 使用 Redis 的 Lua 脚本功能,将
incrBy
和expire
操作封装在一个原子操作中,确保这两个操作要么全部成功,要么全部失败,从而避免并发问题。
- 使用 Redis 的 Lua 脚本功能,将
示例如下:
String luaScript = "local currentVal = redis.call('incrby', KEYS[1], ARGV[1]) " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return currentVal";
List<String> keys = Arrays.asList(key);
Long seqNo = redisTemplateService.execute(new DefaultRedisScript<>(luaScript, Long.class), keys, 1L, 24 * 60 * 60);
通过这种方式,可以确保 incrBy
和 expire
操作是原子性的,避免了并发问题。