目录
1 关于spring cache
SpringCache是Spring框架提供的一种缓存抽象层,可以让开发者方便地实现缓存功能。
下面是针对SpringCache中三个常见问题的
简单解决方案:
-
缓存穿透问题:
指访问一个不存在的key,缓存和数据库都没有数据,导致请求绕过缓存直接访问数据库,容易引起数据库压力过大。解决方案是在缓存中预先设置一个空对象或者null值来占位,避免对数据库的频繁访问。
@Cacheable(value = "userCache", key = "#id") public User getUserById(Long id) { User user = redisTemplate.opsForValue().get(id); if (user == null) { user = userRepository.findById(id); if (user != null) { redisTemplate.opsForValue().set(id, user); } else { redisTemplate.opsForValue().set(id, "", 5, TimeUnit.MINUTES); // 预设空值,有效期5分钟 } } return user; }
-
缓存击穿问题:
指缓存中的一个热点key失效,导致大量的请求直接绕过缓存访问数据库,容易引起数据库压力过大。解决方案是设置缓存时效,采用分布式锁来避免多个请求同时访问数据库。
@Cacheable(value = "userCache", key = "#id", unless = "#result == null") public User getUserById(Long id) { User user = redisTemplate.opsForValue().get(id); if (user == null) { synchronized (this) { user = redisTemplate.opsForValue().get(id); if (user == null) { user = userRepository.findById(id); if (user != null) { redisTemplate.opsForValue().set(id, user, 10, TimeUnit.MINUTES); // 缓存10分钟 } else { redisTemplate.opsForValue().set(id, "", 5, TimeUnit.MINUTES); // 预设空值,有效期5分钟 } } } } return user; }
-
缓存雪崩问题:
指缓存中的大量数据在同一时间过期或失效,导致大量请求直接绕过缓存访问数据库,容易引起数据库压力过大。解决方案是将缓存失效时间随机化,避免大量缓存同时失效;使用多级缓存策略,将缓存分散在不同的节点上;预热缓存,在系统低峰期加载缓存。
@Cacheable(value = "userCache", key = "#id", unless = "#result == null") public User getUserById(Long id) { User user = redisTemplate.opsForValue().get(id); if (user == null) { synchronized (this) { user = redisTemplate.opsForValue().get(id); if (user == null) { user = userRepository.findById(id); if (user != null) { long randomTime = ThreadLocalRandom.current().nextLong(5, 10); // 随机过期时间 redisTemplate.opsForValue().set(id, user, randomTime, TimeUnit.MINUTES); // 随机过期时间缓存 } else { redisTemplate.opsForValue().set(id, "", 5, TimeUnit.MINUTES); // 预设空值,有效期5分钟 } } } } return user; }
2 另一种解决方案:
1 读模式
缓存穿透:查询一个null数据。解决方案:缓存空数据,
可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
@Cacheable(value={"category"},key = "#root.method.name",sync = true)
2 写模式(缓存与数据库一致) canal
读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行;
使用读写锁和Canal来实现缓存的读写同步,可以有效提高缓存的命中率,同时保证数据的一致性。
具体实现步骤如下:
1. 使用读写锁(ReadWriteLock)来保证缓存的读写操作是线程安全的。在获取缓存数据时,首先尝试从缓存中读取,如果缓存中不存在,则从数据库中获取数据,并将数据写入缓存。在写数据时,先将数据更新到数据库中,再更新到缓存中。在读写操作时,使用读锁和写锁来保证线程安全。
2. 引入Canal,感知MySQL的更新操作,从而及时更新缓存。Canal是一个基于MySQL数据库增量日志解析器,可以通过监听MySQL数据库的binlog日志,将增量数据提供给上层应用使用,从而实现对MySQL的实时监控和同步。
代码示例:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private ReadWriteLock lock = new ReentrantReadWriteLock();
private static final String USER_CACHE_KEY = "user_cache";
@Override
public User getUserById(Long id) {
User user = null;
lock.readLock().lock();
try {
user = (User) redisTemplate.opsForHash().get(USER_CACHE_KEY, id.toString());
} finally {
lock.readLock().unlock();
}
if (user == null) {
lock.writeLock().lock();
try {
user = (User) redisTemplate.opsForHash().get(USER_CACHE_KEY, id.toString());
if (user == null) {
user = userDao.getUserById(id);
redisTemplate.opsForHash().put(USER_CACHE_KEY, id.toString(), user);
}
} finally {
lock.writeLock().unlock();
}
}
return user;
}
@Override
public void updateUser(User user) {
userDao.updateUser(user);
lock.writeLock().lock();
try {
redisTemplate.opsForHash().put(USER_CACHE_KEY, user.getId().toString(), user);
} finally {
lock.writeLock().unlock();
}
}
// 监听MySQL的更新操作,更新缓存中的数据
@Override
public void updateCacheByCanal(Long id) {
User user = userDao.getUserById(id);
lock.writeLock().lock();
try {
redisTemplate.opsForHash().put(USER_CACHE_KEY, id.toString(), user);
} finally {
lock.writeLock().unlock();
}
}
}
在这个示例中,我们使用了读写锁来保证缓存的读写操作线程安全,同时引入了Canal,通过监听MySQL的binlog日志,感知到MySQL的更新操作,从而及时更新缓存数据