如何确保缓存失效时数据一致性?代码讲解

场景描述

小明的项目里有一个用户信息的缓存和数据库操作。我们需要确保当用户信息更新时,缓存和数据库的数据始终保持一致。

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 缓存可以互相补充。


总结

小明: 哇,听了你的讲解,又看了这些代码,感觉豁然开朗!原来解决缓存和数据库一致性问题有这么多方法。

你: 哈哈,那当然啦!这些方法各有优缺点,你可以根据实际情况选择最适合的方案。不过,最重要的还是要多实践,多调试,这样才能真正掌握这些技巧。

小明: 好嘞,那我这就去试试!要是还有问题,我再找你!

你: 没问题,随时欢迎!加油,小明!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值