今天是:
带着程序的旅程,每一行代码都是你前进的一步,每个错误都是你成长的机会,最终,你将抵达你的目的地。
title

redis和mysql之间的一致性

1.为什么要使用redis

使用redis缓存可以提高数据访问速度,但是缓存实在内存中的,如果数据全存在redis中显然是不现实的,一般会存储一些经常访问的数据,不会经常变动的数据,这样可以减少请求之间访问数据库带来的时间消耗。

2.使用redis 带来的问题

  1. 内存消耗: Redis是基于内存的存储系统,如果存储的数据量过大,可能会占用大量内存,导致系统资源耗尽。

  2. 数据一致性: Redis的内存存储特性使得它在出现故障时可能会出现数据丢失的情况。或者和数据库中保存的数据不一致,导致业务发生逻辑性错误。

  3. 缓存穿透: 我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。

  4. 缓存击穿: 当某个缓存失效时,可能会导致大量请求直接访问数据库,造成数据库负载急剧增加。可以通过加锁、设置热点数据不过期等方式来避免缓存击穿。

  5. 缓存雪崩: 缓存雪崩(Cache Avalanche)是指在缓存中大量缓存数据同时失效或过期,导致大量的请求直接访问后端数据源,从而引起数据库负载剧增,甚至可能导致系统崩溃的现象。缓存雪崩通常是由于缓存中的数据同时失效,或者在某个特定时间点缓存数据大量过期引起的。

3.一致性问题和解决

 

3.1 先删除缓存在更新数据库。

该种模式下请求A查询库中旧数据并设置缓存后,写请求B更新数据会造成数据不一致.

解决策略 :

3.1.1 缓存设置过期时间,通过设置过期时间,过期时间到了之后会从数据库重新加载,尽管会有一定的延迟,但是可以到达最终一致性。

3.1.2 采用延迟双删策略,更新数据之后睡眠一会,然后再次执行删除。问题在于计算睡眠的时间

3.2 先写数据库在删除缓存,只有在极少数情况下会产生不一致情况,并且会有删除失败的情况

 

如下图,有两种情况

1.T1-T2请求C从数据库查询到旧的数据,然后写请求更新并且设置缓存,之后请求C用旧的数据覆盖新数据到缓存中。

2.T0-T2(主从的情况)写请求先更新数据库并删除缓存,C从库中查询由于延迟仍然查询到了旧值将它设置到缓存中,查询后主从复制将新数据写入到了从库。

解决策略:

3.2.1  订阅mysql binlog 日志 ,然后异步删除缓存。

3.2.2 使用锁机制 更新数据的时候使用写锁,读取数据时使用读锁。

3.2.3 使用事务,通过事务的特性,确保 更新数据和删除redis 同时成功。

使用锁或事务会使性能降低


 

3.3缓存更新的情况

读写缓存:在缓存中执行添加、删除和更改操作,并采用相应的回写策略将数据同步到数据库。

同步写入:使用事务确保缓存和数据更新的原子性,并执行失败重试(如果 Redis 本身失败,将降低服务的性能和可用性)。

异步写入:在写入缓存时,数据库不会同步写入,通过批量异步的方式将其写回数据库(在写回数据库之前,缓存失败会导致数据丢失)。

如下是异步写入数据库的时序图,该种方式也会产生不一致情况,并且同步数据库会有一定的延迟,适合对一致性要求不高的场景

一种异步写入的方式


/**
 * 以缓存为中心,读取时如果缓存没有从数据库获取,并保存到缓存钟,更新时先设置到缓存,然后异步更新数据库
 */

@Slf4j
@Component
@EnableScheduling
public class WriteBehind {
    @Autowired
    private AccountRepository accountRepository;


    @Autowired
    RedisTemplate redisTemplate;

    
    private final String cache_key="account";
    @Autowired
    RQueue<Account> accountRQueue;


    public Account getAccountById(Integer id) {
        if(redisTemplate.opsForValue().get(cache_key+"::"+id)!=null){
            return  (Account)redisTemplate.opsForValue().get(cache_key+"::"+id);
        }else{
            Optional<Account> byId = accountRepository.findById(id);
            byId.ifPresent(account -> redisTemplate.opsForValue().set(cache_key + "::" + id, account));
            return byId.get();
        }
    }

    public void evictAccountCache(Integer id) {
        redisTemplate.delete(cache_key + "::" + id);
    }

    public void updateAccount(Account account) {
        redisTemplate.opsForValue().set(cache_key+"::"+account.getId(),account);
        accountRQueue.add(account);
    }


    @Scheduled(fixedDelay = 1000, initialDelay = 2000)
    public void persist() {
        log.info("持久开始。。");
        while(!accountRQueue.isEmpty()){
            Account pool = accountRQueue.poll();
            if(pool!=null){
                accountRepository.save(pool);
            }
        }
    }

}

代码地址:consistency-problem/redis-mysql

 4.总结

对于每种不同的缓存更新策略,没办法完全保证一致性,只能按需求选择最合适的缓存和数据库更新策略,最大可能保证一致性。同时要保证性能。

 

 

 

分享到:

专栏

类型标签

网站访问总量