java
复制代码
package com.study.sredis.stept001.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.dto.RedisData;
import com.study.sredis.stept001.mapper.userMapper;
import com.study.sredis.stept001.service.User5Service;
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.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* 缓存击穿
* 定义:指一个热点key在缓存过期的瞬间,同时有大量请求访问这个key,导致所有请求都直接打到数据库上,造成数据库压力激增
* 解决方案:
* (1)互斥锁 (Mutex Lock)
* 核心思想:只允许一个线程去查询数据库,其他线程等待
* 实现方式:使用Redis的setnx命令或Redisson分布式锁
* (2)热点数据永不过期
* 核心思想:对热点数据不设置过期时间,通过其他机制更新
* 实现方式:逻辑过期时间 + 异步更新
* (3)接口限流与降级
* 核心思想:控制访问数据库的并发量
* 实现方式:使用限流组件如Sentinel
*/
@Service
public class User5ServiceImpl extends ServiceImpl<userMapper, User> implements User5Service {
@Autowired
private userMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final String PRODOUCT_KEY_PREFIX = "product";
private static final String LOCK_KEY_PREFIX = "lock:product";
private static final Duration CACHE_TIMEOUT = Duration.ofMinutes(30);
private static final Duration LOCK_TIMEOUT = Duration.ofSeconds(10);
/**
* 解决方案一:互斥锁
*
* @param user
* @return
*/
@Override
public User getUserWithMutex(User user) {
String cachekey = PRODOUCT_KEY_PREFIX + user.getId();
// 1.从缓存查询
User user1 = getFromCache(cachekey);
if (user1 != null && user1.getId() != null) {
return user1;
}
// 2.获取分布式锁
String lockKey = LOCK_KEY_PREFIX + user.getId();
boolean locked = false;
try {
// 尝试获取锁
locked = tryLock(lockKey);
if (locked) {
// 3.双重检查缓存
user1 = getFromCache(cachekey);
if (user1 != null && user1.getId() != null) {
return user1;
}
// 4.查询数据库
user1 = this.getById(user.getId());
if (user1 != null) {
// 写入缓存
setCache(cachekey, user1, CACHE_TIMEOUT);
} else {
// 缓存空值防止穿透
setCache(cachekey, new User(), Duration.ofMinutes(5));
}
return user1;
} else {
// 获取锁失败,等待后重试
Thread.sleep(50);
return getUserWithMutex(user);
}
} catch (Exception e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取用户信息失败", e);
} finally {
if (locked) {
releaseLock(lockKey);
}
}
}
private User getFromCache(String key) {
try {
return (User) redisTemplate.opsForValue().get(key);
} catch (Exception e) {
return null;
}
}
private void setCache(String key, Object value, Duration timeout) {
try {
redisTemplate.opsForValue().set(key, value, timeout);
} catch (Exception e) {
}
}
private boolean tryLock(String lockKey) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_TIMEOUT)
);
}
private void releaseLock(String lockKey) {
try {
redisTemplate.delete(lockKey);
} catch (Exception e) {
}
}
/**
* 解决方案二:分布式锁
*
* @param user
* @return
*/
@Override
public User getUserWithRedissonLock(User user) {
String cacheKey = PRODOUCT_KEY_PREFIX + user.getId();
// 1.从缓存中查询
User user1 = getFromCache(cacheKey);
if (user1 != null && user1.getId() != null) {
return user1;
}
String lockKey = LOCK_KEY_PREFIX + user.getId();
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,等待时间3秒,锁超时时间10秒
boolean isLock = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (isLock) {
try {
// 3.双重检查
user1 = getFromCache(cacheKey);
if (user1 != null && user1.getId() != null) {
return user1;
}
// 4.查询数据库
user1 = this.getById(user.getId());
if (user1 != null) {
setCache(cacheKey, user1, CACHE_TIMEOUT);
} else {
setCache(cacheKey, new User(), Duration.ofMinutes(5));
}
return user1;
} finally {
lock.unlock();
}
} else {
// 获取锁失败,等待后重试
Thread.sleep(100);
return getUserWithRedissonLock(user);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取用户信息失败", e);
}
}
/**
* 解决方案三:逻辑过期
*
* @param user
* @return
*/
@Override
public User getUserWithLogicalExpire(User user) {
String cacheKey = PRODOUCT_KEY_PREFIX + user.getId();
// 1.从缓存查询包装数据
RedisData<User> redisData = getRedisDataFromCache(cacheKey);
if (redisData == null) {
// 缓存不存在,直接查询并设置
return getAndSetProduct(user);
}
User user1 = redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
// 2.判断是否逻辑过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,直接返回
return user1;
}
// 3.已过期, 尝试获取锁重建缓存
String lockKey = LOCK_KEY_PREFIX + user.getId();
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock()) {
try {
// 4.双重检查
redisData = getRedisDataFromCache(cacheKey);
if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return redisData.getData();
}
// 5.异步重建缓存
CompletableFuture.runAsync(() -> {
rebuildUserCache(user);
});
} finally {
lock.unlock();
}
}
// 返回旧数据
return user1;
}
private User getAndSetProduct(User user) {
User user1 = this.getById(user.getId());
if (user1 != null) {
RedisData<User> redisData = new RedisData<>(
user1,
LocalDateTime.now().plusMinutes(30) //逻辑过期时间30分钟
);
setRedisDataCache(PRODOUCT_KEY_PREFIX + user.getId(), redisData);
}
return user1;
}
@Async
public void rebuildUserCache(User user) {
String lockKey = LOCK_KEY_PREFIX + user.getId();
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock()) {
try {
// 再次检查,防止重复更新
User user1 = this.getById(user.getId());
if (user1 != null) {
RedisData<User> redisData = new RedisData<>(
user1,
LocalDateTime.now().plusMinutes(30)
);
setRedisDataCache(PRODOUCT_KEY_PREFIX + user.getId(), redisData);
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
private RedisData<User> getRedisDataFromCache(String key) {
try {
return (RedisData<User>) redisTemplate.opsForValue().get(key);
} catch (Exception e) {
return null;
}
}
private void setRedisDataCache(String key, RedisData<User> redisData) {
try {
// 永不过期,使用逻辑过期时间控制
redisTemplate.opsForValue().set(key, redisData);
} catch (Exception e) {
}
}
@Override
public User getHotUser(User user) {
return null;
}
@Override
public boolean updateUser(User user) {
return false;
}
}