场景描述
小明的项目里有一个用户信息的缓存和数据库操作。我们需要确保当用户信息更新时,缓存和数据库的数据始终保持一致。
1. 双写一致性
思路: 在更新数据库的同时,更新缓存。
代码示例:
@Service
publicclass UserService {
@Autowired
private UserRepository userRepository; // 数据库操作
@Autowired
private RedisTemplate<String, Object> redisTemplate; // Redis操作
@Transactional// 保证数据库和缓存操作在一个事务中
public void updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 更新缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
}
小明: 哇,这个@Transactional
看起来很厉害!那是不是只要用了这个,就万无一失了?
你: 哈哈,理论上是这样的,但网络延迟或者缓存更新失败的情况还是有可能出现的。不过,这个方法是最简单直接的,能解决大部分问题。
2. 延时双删
思路: 当缓存失效时,先标记缓存键,延时删除,避免缓存穿透。
代码示例:
@Service
publicclass UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public User getUser(Long id) {
// 尝试从缓存获取
User user = (User) redisTemplate.opsForValue().get("user:" + id);
if (user == null) {
// 缓存失效,从数据库获取
user = userRepository.findById(id);
// 更新缓存
redisTemplate.opsForValue().set("user:" + id, user, 30, TimeUnit.SECONDS);
}
return user;
}
public void deleteUser(Long id) {
// 删除数据库记录
userRepository.deleteById(id);
// 标记缓存失效
redisTemplate.opsForValue().set("user:" + id, null, 5, TimeUnit.SECONDS);
}
}
小明: 哈哈哈,这个“延时删除”有点意思!那要是有人在这5秒内访问怎么办?
你: 哈哈,别急,这5秒内访问的话,会直接从数据库读取数据,然后更新缓存。这样就能保证数据的准确性啦。
3. 发布/订阅机制
思路: 数据库更新后,发布消息,监听消息的线程更新缓存。
代码示例:
@Service
publicclass UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisMessagePublisher publisher; // 消息发布器
public void updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 发布消息,通知缓存更新
publisher.publish("update:user:" + user.getId());
}
}
@Component
publicclass RedisMessagePublisher {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void publish(String message) {
redisTemplate.convertAndSend("userChannel", message);
}
}
@Component
publicclass RedisMessageSubscriber implements MessageListener {
@Autowired
private UserService userService;
@Override
public void onMessage(Message message, byte[] pattern) {
String msg = new String(message.getBody());
if (msg.startsWith("update:user:")) {
Long userId = Long.parseLong(msg.split(":")[2]);
// 根据用户ID更新缓存
userService.getUser(userId);
}
}
}
小明: 哇,这个发布/订阅机制看起来很复杂啊!不过好像很厉害的样子。
你: 哈哈,确实有点复杂,但它能很好地解决缓存和数据库之间的同步问题。而且,这种解耦的方式让代码更清晰。
4. 本地缓存兜底
思路: 在本地缓存中再存一份数据,当 Redis 缓存失效时,从本地缓存读取。
代码示例:
@Service
publicclass UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
privatefinal Map<Long, User> localCache = new ConcurrentHashMap<>();
public User getUser(Long id) {
// 尝试从 Redis 缓存获取
User user = (User) redisTemplate.opsForValue().get("user:" + id);
if (user == null) {
// Redis 缓存失效,尝试从本地缓存获取
user = localCache.get(id);
if (user == null) {
// 本地缓存也失效,从数据库获取
user = userRepository.findById(id);
// 更新 Redis 缓存和本地缓存
redisTemplate.opsForValue().set("user:" + id, user, 30, TimeUnit.SECONDS);
localCache.put(id, user);
}
}
return user;
}
public void updateUser(User user) {
// 更新数据库
userRepository.save(user);
// 更新 Redis 缓存
redisTemplate.opsForValue().set("user:" + user.getId(), user, 30, TimeUnit.SECONDS);
// 更新本地缓存
localCache.put(user.getId(), user);
}
}
小明: 哈哈哈,这个本地缓存兜底听起来很靠谱啊!那是不是本地缓存和 Redis 缓存都得维护?
你: 哈哈,没错!不过,本地缓存的维护成本相对较低,而且可以快速响应。只要合理设计,本地缓存和 Redis 缓存可以互相补充。
总结
小明: 哇,听了你的讲解,又看了这些代码,感觉豁然开朗!原来解决缓存和数据库一致性问题有这么多方法。
你: 哈哈,那当然啦!这些方法各有优缺点,你可以根据实际情况选择最适合的方案。不过,最重要的还是要多实践,多调试,这样才能真正掌握这些技巧。
小明: 好嘞,那我这就去试试!要是还有问题,我再找你!
你: 没问题,随时欢迎!加油,小明!