Redis使用分布式锁和看门狗机制实现访问量统计
1. 配置 Redis
确保Spring Boot 项目中已经包含了 Redis 和redisson的依赖。在 pom.xml 中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.0</version>
</dependency>
2. 配置 Redisson
创建一个配置类来配置 Redisson
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
3. 创建访问量服务
在服务类中使用 Redisson 分布式锁来确保线程安全:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class PageViewService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final String PAGE_VIEW_KEY_PREFIX = "page:view:";
private static final String LOCK_KEY_PREFIX = "lock:page:view:";
public void incrementPageView(String pageId) {
String redisKey = PAGE_VIEW_KEY_PREFIX + pageId;
String lockKey = LOCK_KEY_PREFIX + pageId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,等待时间为2秒,锁的持有时间为10秒
if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {
try {
// 如果Redis中没有该页面的访问量,则从数据库中读取并初始化
Object value = redisTemplate.opsForValue().get(redisKey);
if (value == null) {
// 假设从数据库中读取初始访问量数据
int initialViewCount = getInitialPageViewFromDatabase(pageId);
redisTemplate.opsForValue().set(redisKey, initialViewCount);
}
// 增加访问量
redisTemplate.opsForValue().increment(redisKey);
} finally {
// 确保锁的释放
lock.unlock();
}
} else {
// 锁等待超时的处理逻辑(可选)
System.out.println("Failed to acquire lock for page: " + pageId);
}
} catch (InterruptedException e) {
e.printStackTrace();
// 处理锁获取失败的情况
}
}
public int getPageView(String pageId) {
Object value = redisTemplate.opsForValue().get(PAGE_VIEW_KEY_PREFIX + pageId);
return value != null ? Integer.parseInt(value.toString()) : 0;
}
private int getInitialPageViewFromDatabase(String pageId) {
// 假设从数据库中读取初始访问量数据
return 0; // 返回初始访问量
}
}
调用incrementPageView(pageId)就能在redis里面自增
调用getPageView(pageId)获取redis中访问量
这里设置等待锁的时间为 2 秒,如果在 2 秒内无法获取锁,则返回 false,避免长时间等待。
但是这种固定10秒的锁可能会出现并发问题。比如:
-
锁自动释放后新的请求获取锁:
在锁自动释放后,其他等待的请求会有机会获取到锁。这可能会导致并发请求对同一个资源进行操作,可能出现数据一致性问题。 -
未完成的业务逻辑被中断:
原先持有锁的线程可能会在业务逻辑尚未完成的情况下失去锁,这样可能会导致未完成的操作影响系统的稳定性和数据的一致性。
可以使用 Redisson 看门狗机制,进行续锁,改进后:
public void incrementPageView(String pageId) {
String redisKey = PAGE_VIEW_KEY_PREFIX + pageId;
String lockKey = LOCK_KEY_PREFIX + pageId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
// 锁定成功后启动看门狗
try {
boolean renewLock = true;
while (renewLock) {
// 定期续期锁
renewLock = lock.renewLease(10, TimeUnit.SECONDS);
// 执行业务逻辑
Object value = redisTemplate.opsForValue().get(redisKey);
if (value == null) {
int initialViewCount = getInitialPageViewFromDatabase(pageId);
redisTemplate.opsForValue().set(redisKey, initialViewCount);
}
redisTemplate.opsForValue().increment(redisKey);
// 检查是否需要继续续期
if (/* 业务逻辑完成条件 */) {
renewLock = false;
}
}
} finally {
lock.unlock();
}
} else {
System.out.println("Failed to acquire lock for page: " + pageId);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
大概逻辑就这样,具体看业务需求,俺只做个笔记!
目录