关于SpringCache某些问题的简单解决方案

目录

1 关于spring cache

简单解决方案:

2 另一种解决方案:

1 读模式

2 写模式(缓存与数据库一致) canal


1 关于spring cache

SpringCache是Spring框架提供的一种缓存抽象层,可以让开发者方便地实现缓存功能。

下面是针对SpringCache中三个常见问题的

简单解决方案:

  1. 缓存穿透问题: 
    指访问一个不存在的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;
    }
    

  2. 缓存击穿问题:
    指缓存中的一个热点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;
    }
    

  3. 缓存雪崩问题:
    指缓存中的大量数据在同一时间过期或失效,导致大量请求直接绕过缓存访问数据库,容易引起数据库压力过大。解决方案是将缓存失效时间随机化,避免大量缓存同时失效;使用多级缓存策略,将缓存分散在不同的节点上;预热缓存,在系统低峰期加载缓存。
     

    @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的更新操作,从而及时更新缓存数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值