Redis在项目实践中的问题解决方案汇总

news2024/11/20 6:22:40

前言

无论是在开发过程中还是在准备跑路的面试过程中,和Redis相关的话题,难免会涉及到四个特殊场景:缓存穿透、缓存雪崩、缓存击穿以及数据一致性。

虽然在作为服务缓存层的时候Redis确实能极大减少服务端的请求压力,但是如果在开发中不注意这些场景的话,在高并发场景下有可能会导致系统崩溃,数据错乱等情况。Now,笔者结合学习过程中的一些实际的业务场景来复现并解决这些问题。

缓存穿透

缓存穿透是指查询一个不存在的数据,在缓存层和持久层都查询不到,这样查不到的数据不会写入缓存,这样一个不存在的数据每次的请求都会去查询持久层,缓存也就失去了保护后端持久层的意义了。最严重的后果就是是后端存储负载加大,造成后端存储宕机。

 比如现在对一条测试数据进行缓存,一般的逻辑是先查询缓存中是否存在该数据,如果存在则直接返回,否则再查询数据库并将查询结果进行缓存。这里用注解的方式实现缓存。

  @Cacheable(key = "#id")
    public RedisTest findById(Integer id) {
        RedisTest all = redisTestDao.findAllByIdRedisTest(id);
        log.info("查询数据库---》{}",id);
        return StringUtils.isEmpty(all)?null:all;

一般情况而言,并没有什么不妥之处。但是如果出现一些恶意攻击的情况,出现大量请求去查询一个并不存在的数据,如果设置结果为null时不进缓存就会大量查询数据库,严重造成数据库宕机。如果进入缓存,就会使空数据与正常数据一样过期,一样造成内存浪费。

解决方案1:对空值设置更短的过期时长

  @Cacheable(key = "#id")
    public RedisTest findById(Integer id) {
        RedisTest all = redisTestDao.findAllByIdRedisTest(id);
        log.info("查询数据库---》{}",id);
        if (StringUtils.isEmpty(all)){
            redisTemplate.opsForValue().set(String.valueOf(id),"", Duration.ofMinutes(5));
            return null;
        }
        return all;
    }

这样查询一次数据库后,后续全部存放空数据,并且空数据能更快清除,而不影响内存释放。

2023-04-25 17:21:40.836  INFO 21712 --- [nio-8090-exec-2] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:40.836--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8ade77b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1951093559 wrapping com.mysql.cj.jdbc.ConnectionImpl@4c983b3a] will not be managed by Spring
==>  Preparing: select * from redis_test where id =?
==> Parameters: 1(Integer)
<==      Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@8ade77b]
2023-04-25 17:21:40.910  INFO 21712 --- [nio-8090-exec-2] c.y.r.service.impl.RedisTestServiceImpl  : 查询数据库---》1
2023-04-25 17:21:52.178  INFO 21712 --- [nio-8090-exec-7] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.178--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [nio-8090-exec-2] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [nio-8090-exec-1] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [nio-8090-exec-8] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [nio-8090-exec-5] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [nio-8090-exec-4] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 17:21:52.180  INFO 21712 --- [io-8090-exec-10] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T17:21:52.180--访问接口:R com.yy.redisCache.controller.RedisCacheController.getById(Integer)
​

解决方案2:布隆过滤器

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

Bloom Filter 原理

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

Bloom Filter的缺点

bloom filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性。

  • 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。

  • 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。

Bloom Filter的实现

在使用布隆过滤器时有两个核心参数,分别是预估的数据量size以及期望的误判率fpp,这两个参数我们可以根据自己的业务场景和数据量进行自主设置。在实现布隆过滤器时,有两个核心问题,分别是hash函数的选取个数n以及确定bit数组的大小len。

1,根据预估数据量size和误判率fpp,可以计算出bit数组的大小len。

 2.根据预估数据量size和bit数组的长度大小len,可以计算出所需要的hash函数个数n。

 

项目中这里用Hutool工具包中布隆过滤器来实现,通过指定BitMap就能创建一个BitMapBloomFilter来使用。

Step1:创建BitMapBloomFilter的Bean

创建一个配置类,指定BitMap大小、创建过滤器对象并注入Spring容器。

@Configuration
public class RedisConfig {
    
    @Bean
    public BitMapBloomFilter bitMapBloomFilter(){
        //指定布隆过滤器BitMap的大小
        return new BitMapBloomFilter(10);
    }

Step2:初始化布隆过滤器中的数据

将需要查询的数据放入布隆过滤器,这样进行查询时会先通过过滤器过滤不存在的查询条件,然后再查缓存或数据库。

package com.yy.redisCache.util;

import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.core.collection.CollUtil;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.impl.RedisTestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;

/**
 * @author young
 * Date 2023/4/26 10:30
 * Description: 初始化布隆过滤器,将查询的id对应的key存入过滤器
 */
@Component
public class BloomFilterInitData {

    @Resource
    private RedisTestServiceImpl redisTestService;
    @Resource
    private BitMapBloomFilter bitMapBloomFilter;

    @PostConstruct
    public void initMethod() {
        List<RedisTest> testList = redisTestService.lambdaQuery().select(RedisTest::getId).list();
        if (CollUtil.isNotEmpty(testList)) {
            testList.stream().map(data -> {
                return "test::" + data.getId();
            }).forEach(bitMapBloomFilter::add);
        }
    }
}

Step3:修改业务类中的查询逻辑

@Service
@Slf4j
@CacheConfig(cacheNames = "test")//缓存名,和管理器中配置的一致
public class RedisTestServiceImpl extends ServiceImpl<RedisTestDao, RedisTest> implements RedisTestService {
    @Resource
    private RedisTestDao redisTestDao;
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Resource
    private BitMapBloomFilter bitMapBloomFilter;
    @Override
    public RedisTest bloomData(Integer id) {
        String key = "test::" + id;
        boolean has = bitMapBloomFilter.contains(key);
        if (!has){
            log.info("数据不存在,过滤----" );
            return null;
        }
        String s = redisTemplate.opsForValue().get(key);
        RedisTest redisTest = null;
        if (CharSequenceUtil.isNotEmpty(s)){
            log.info("查询缓存----");
            redisTest = JSONUtil.toBean(s,RedisTest.class);
        }else {
            log.info("查询数据库----" );
            redisTest = lambdaQuery().eq(RedisTest::getId,id).one();
            if (ObjectUtil.isNotEmpty(redisTest)){
                redisTemplate.opsForValue().set("test::" + id,JSONUtil.toJsonStr(redisTest),2L, TimeUnit.MINUTES);
            }
        }
        return redisTest;
    }
}

这样面对大量不存在的数据执行查询访问时,先通过布隆过滤器过滤一些没有结果的数据,防止直接查询数据库或进入缓存,然后将有结果的数据查询数据库后放入缓存,一定程度上减少了缓存内存压力,又防止多次直接操作数据库层。

Step4:测试

创建控制层接口,进行接口测试。

  @GetMapping("get/bloom")
    public R<RedisTest> bloom(@RequestParam Integer id){
        RedisTest test = service.bloomData(id);
        if (ObjectUtil.isNotEmpty(test)){
            return R.ok(test);
        }  return R.fail(null);
    }

通过测试工具ApiPost进行自动化,模拟20条请求,id范围为1~5(随机),其中1~3的id无数据。

 测试成功后查看日志可发现,在并行请求时,一次请求时间执行内其他的请求也会查询数据库,但是有数据的部分放入缓存后,后期就之查缓存就行了,而不存在结果的数据已经被布隆过滤器完全过滤了。

2023-04-26 15:12:42.269  INFO 19080 --- [io-8090-exec-22] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.269--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.269  INFO 19080 --- [io-8090-exec-22] c.y.r.controller.RedisCacheController    : 进入测试的id---》2
2023-04-26 15:12:42.269  INFO 19080 --- [io-8090-exec-22] c.y.r.service.impl.RedisTestServiceImpl  : 数据不存在,过滤----
2023-04-26 15:12:42.298  INFO 19080 --- [nio-8090-exec-4] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.298--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.300  INFO 19080 --- [nio-8090-exec-4] c.y.r.controller.RedisCacheController    : 进入测试的id---》3
2023-04-26 15:12:42.300  INFO 19080 --- [nio-8090-exec-4] c.y.r.service.impl.RedisTestServiceImpl  : 数据不存在,过滤----
2023-04-26 15:12:42.316  INFO 19080 --- [io-8090-exec-26] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.316--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.318  INFO 19080 --- [io-8090-exec-26] c.y.r.controller.RedisCacheController    : 进入测试的id---》3
2023-04-26 15:12:42.318  INFO 19080 --- [io-8090-exec-26] c.y.r.service.impl.RedisTestServiceImpl  : 数据不存在,过滤----
2023-04-26 15:12:42.335  INFO 19080 --- [io-8090-exec-21] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.335--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.336  INFO 19080 --- [io-8090-exec-21] c.y.r.controller.RedisCacheController    : 进入测试的id---》4
2023-04-26 15:12:42.359  INFO 19080 --- [io-8090-exec-21] c.y.r.service.impl.RedisTestServiceImpl  : 查询数据库----
2023-04-26 15:12:42.408  INFO 19080 --- [io-8090-exec-23] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.408--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.408  INFO 19080 --- [io-8090-exec-23] c.y.r.controller.RedisCacheController    : 进入测试的id---》4
2023-04-26 15:12:42.431  INFO 19080 --- [io-8090-exec-23] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-26 15:12:42.449  INFO 19080 --- [nio-8090-exec-9] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.449--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.449  INFO 19080 --- [nio-8090-exec-9] c.y.r.controller.RedisCacheController    : 进入测试的id---》4
2023-04-26 15:12:42.472  INFO 19080 --- [nio-8090-exec-9] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-26 15:12:42.489  INFO 19080 --- [io-8090-exec-25] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.488--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.489  INFO 19080 --- [io-8090-exec-25] c.y.r.controller.RedisCacheController    : 进入测试的id---》4
2023-04-26 15:12:42.511  INFO 19080 --- [io-8090-exec-25] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-26 15:12:42.530  INFO 19080 --- [io-8090-exec-14] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.530--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.531  INFO 19080 --- [io-8090-exec-14] c.y.r.controller.RedisCacheController    : 进入测试的id---》3
2023-04-26 15:12:42.531  INFO 19080 --- [io-8090-exec-14] c.y.r.service.impl.RedisTestServiceImpl  : 数据不存在,过滤----
2023-04-26 15:12:42.549  INFO 19080 --- [nio-8090-exec-8] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.549--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.549  INFO 19080 --- [nio-8090-exec-8] c.y.r.controller.RedisCacheController    : 进入测试的id---》3
2023-04-26 15:12:42.549  INFO 19080 --- [nio-8090-exec-8] c.y.r.service.impl.RedisTestServiceImpl  : 数据不存在,过滤----
2023-04-26 15:12:42.566  INFO 19080 --- [io-8090-exec-24] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-26T15:12:42.566--访问接口:R com.yy.redisCache.controller.RedisCacheController.bloom(Integer)
2023-04-26 15:12:42.566  INFO 19080 --- [io-8090-exec-24] c.y.r.controller.RedisCacheController    : 进入测试的id---》5
2023-04-26 15:12:42.589  INFO 19080 --- [io-8090-exec-24] c.y.r.service.impl.RedisTestServiceImpl  : 查询数据库----

缓存击穿

当某个key成为一个热点(商品秒杀时)时,这样处于一个集中式高并发的情况下,如果key突然失效一瞬间,请求就会马上击穿缓存层,直接请求数据库,这样后端负载会很快过载甚至崩溃。这样就形成了缓存击穿。

针对缓存击穿问题,有两种解决方案,一种是对热点数据不设置过期时间,另一种是采用互斥锁的方式。

解决方案一:热点数据不设置过期时间

热点数据不设置过期时间,当后台更新热点数据数需要同步更新缓存中的数据,这种解决方式适用于不严格要求缓存一致性的场景。实现方式就使不设置ttl即可,不具体演示。

解决方案二:使用互斥锁

如果是单机部署的环境下可以使用synchronized或lock来处理,保证同时只能有一个线程来查询数据库,其他线程可以等待数据缓存成功后在被唤醒,从而直接查询缓存即可。如果是分布式部署,可以采用分布式锁来实现互斥。

package com.yy.redisCache.util;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author young
 * Date 2023/4/26 15:36
 * Description: redis互斥锁实现
 */
@Component
public class RedisLock {
    @Autowired
    private RedisTemplate<String ,String> redisTemplate;

    /**
     * 互斥锁
     * @param k 对应redis中的key
     * @param v 存放的锁标识
     * @param expire 过期时间
     * @return
     */
    public boolean setLock(String k,String v,long expire){
        Boolean absent = redisTemplate.opsForValue().setIfAbsent(k, v, expire, TimeUnit.MINUTES);
        if (Boolean.TRUE.equals(absent)){
            return true;
        }
        //如果线程没有获取锁,则在此处循环获取
        return setLock(k,v,expire);
    }

    /**
     * 释放锁
     * @param k 对应redis中的jey
     * @param v 锁标识
     */
    public void unlock(String k,String v){
        String s = redisTemplate.opsForValue().get(k);
        if (CharSequenceUtil.equals(s,v)){
            //避免锁被其他线程误删
            redisTemplate.delete(k);
        }
    }
}

重新改进业务层代码,在进行数据库查询之前加锁,当数据读取完成后再释放锁。

 @Override
    public RedisTest lockData(Integer id) {
        String key = "test::" + id;
        boolean has = bitMapBloomFilter.contains(key);
        if (!has){
            log.info("数据不存在,过滤----" );
            return null;
        }
        String s = redisTemplate.opsForValue().get(key);
        RedisTest redisTest = null;
        if (CharSequenceUtil.isNotEmpty(s)){
            log.info("查询缓存----");
            redisTest = JSONUtil.toBean(s,RedisTest.class);
        }else {
            //创建锁对应的id标识
            String uid = IdUtil.simpleUUID();
            String lockKey = key+"::lock";
            boolean b = redisLock.setLock(lockKey, uid, 1);
            if (b){
                try{
                    //加锁如果成功先查缓存
                    s = redisTemplate.opsForValue().get(key);
                    if (CharSequenceUtil.isNotEmpty(s)){
                        log.info("查询缓存----");
                        redisTest = JSONUtil.toBean(s,RedisTest.class);
                    }else {
                        log.info("查询数据库----" );
                        redisTest = lambdaQuery().eq(RedisTest::getId,id).one();
                        if (ObjectUtil.isNotEmpty(redisTest)){
                            //设置一秒的缓存测试并发效果
                            redisTemplate.opsForValue().set("test::" + id,JSONUtil.toJsonStr(redisTest),1L, TimeUnit.SECONDS);
                        }
                    }
                }finally {
                    //释放锁
                    redisLock.unlock(lockKey,uid);
                }
            }
        }
        return redisTest;
    }

测试并发条件下的的效果,这里用20个请求模拟并发,实际每秒完成12个请求,也就是说在只设置了1秒缓存的情况下,某个时间段必然会查询数据库,但是在执行这一次数据库操作的时间内其他请求并不会多次查询数据库,而是等缓存重新建立后再查询建立缓存后的数据。

 查看日志,可以看到预期效果实现了,在缓存失效的情况下,仅执行了一次数据库操作,后面请求全部走缓存。

2023-04-28 08:53:04.850  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询数据库----
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33e03af6] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@704636388 wrapping com.mysql.cj.jdbc.ConnectionImpl@57df93c1] will not be managed by Spring
==>  Preparing: SELECT id,redis_name,redis_pwd,address FROM redis_test WHERE (id = ?)
==> Parameters: 21(Integer)
<==    Columns: id, redis_name, redis_pwd, address
<==        Row: 21, redis测试数据21号, 577948, 本地测试数据67
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33e03af6]
2023-04-28 08:53:05.077  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.096  INFO 7264 --- [nio-8090-exec-8] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.116  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.159  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.201  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.223  INFO 7264 --- [nio-8090-exec-8] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----         
2023-04-28 08:53:05.242  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.263  INFO 7264 --- [nio-8090-exec-3] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.264  INFO 7264 --- [nio-8090-exec-8] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.284  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.307  INFO 7264 --- [nio-8090-exec-8] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.326  INFO 7264 --- [nio-8090-exec-6] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.415  INFO 7264 --- [nio-8090-exec-1] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.546  INFO 7264 --- [nio-8090-exec-5] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.674  INFO 7264 --- [nio-8090-exec-9] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.810  INFO 7264 --- [io-8090-exec-10] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:05.964  INFO 7264 --- [nio-8090-exec-7] c.y.r.service.impl.RedisTestServiceImpl  : 查询数据库----
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ffc6cea] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@522751494 wrapping com.mysql.cj.jdbc.ConnectionImpl@57df93c1] will not be managed by Spring
==>  Preparing: SELECT id,redis_name,redis_pwd,address FROM redis_test WHERE (id = ?)
==> Parameters: 21(Integer)
<==    Columns: id, redis_name, redis_pwd, address
<==        Row: 21, redis测试数据21号, 577948, 本地测试数据67
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ffc6cea]
2023-04-28 08:53:06.158  INFO 7264 --- [nio-8090-exec-4] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----
2023-04-28 08:53:06.286  INFO 7264 --- [nio-8090-exec-2] c.y.r.service.impl.RedisTestServiceImpl  : 查询缓存----

这种方式仅限于单机模式条件下,如果系统采用分布式则需要使用分布式锁来实现互斥了,这里不多做说明。

缓存雪崩

缓存雪崩是指对热点数据设置了相同的过期时间,在同一时间这些热点数据key大批量发生过期,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。与缓存击穿不同的是,缓存击穿是单个热点数据过期,而缓存雪崩是大批量热点数据过期。

解决方案一:设置随机的过期时间

将key的过期时间后面加上一个随机数,这个随机数值的范围可以根据自己的业务情况自行设定,这样可以让key均匀的失效,避免大批量的同时失效。

if (ObjectUtil.isNotNull(redisTest)) {
    //生成一个1~5的随机数
    int randomInt = RandomUtil.randomInt(1, 5); 
    //过期时间+随机数形成随机时间
    redisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(redisTest), 2L+randomInt, TimeUnit.SECONDS);
}

解决方案二:不设置过期时间

不设置过期时间时,需要注意的是,在更新数据库数据时,同时也需要更新缓存数据,否则数据会出现不一致的情况。这种方式比较适用于不严格要求缓存一致性的场景。

解决方案三:搭建高可用集群

缓存服务故障时,也会触发缓存雪崩,为了避免因服务故障而发生的雪崩,推荐使用高可用的服务集群,这样即使发生故障,也可以进行故障转移。

数据一致性

通常情况下,使用缓存的直接目的是为了提高系统的查询效率,减轻数据库的压力。一般情况下使用缓存是下面这几步骤:

  1. 查询缓存,数据是否存在

  2. 如果数据存在,直接返回

  3. 如果数据不存在,再查询数据库

  4. 如果数据库中数据存在,那么将该数据存入缓存并返回。如果不存在,返回空。

 一般情况下这个流程并没有太大问题,但是会有一个细节问题:当一条数据存入缓存后,立刻又被修改了,那么这个时候缓存该如何更新呢。不更新肯定不行,这样导致了缓存中的数据与数据库中的数据不一致。一般情况下对于缓存更新有下面这几种情况:

  1. 先更新缓存,再更新数据库

  2. 先更新数据库,再更新缓存

  3. 先删除缓存,再更新数据库

  4. 先更新数据库,再删除缓存

先更新缓存,再更新数据库

先更新缓存,再更新数据库这种情况下,如果业务执行正常,不出现网络等问题,这么操作不会有啥问题,两边都可以更新成功。但是,如果缓存更新成功了,但是当更新数据库时或者在更新数据库之前出现了异常,导致数据库无法更新。这种情况下,缓存中的数据变成了一条实际不存在的假数据。

 模拟一个更新数据库时出现异常的业务情况:

 /**
     * 模拟更新数据库出现异常
     * @param redisTest
     * @return
     */
    public boolean updateButException(RedisTest redisTest){
        String key = "test::"+redisTest.getId();
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisTest),5L,TimeUnit.MINUTES);
        //模拟异常
        int exc =10/0;
        return updateById(redisTest);
    }

构建测试接口:

    public Boolean updateOne(@RequestBody RedisTest redisTest){
        return service.updateButException(redisTest);
    }

测试更新情况并查询数据:

 此时数据库更新出现异常,但是缓存已经更新成功了,再次执行查询操作后检查获取数据和实际数据库数据情况:

 数据不一致的情况就出现了,显然获取的是个假数据。

先更新数据库,再更新缓存

先更新数据库,再更新缓存和先更新缓存,再更新数据库的情况基本一致,如果失败,会导致数据库中是最新的数据,缓存中是旧数据。还有一种极端情况,在高并发情况下容易出现数据覆盖的现象:A线程更新完数据库后,在要执行更新缓存的操作时,线程被阻塞了,这个时候线程B更新了数据库并成功更新了缓存,当B执行完成后线程A继续向下执行,那么最终线程B的数据会被覆盖。此时B查询数据时仍然会出现数据一致性问题,B会在缓存中获得被A覆盖的假数据。

 先删除缓存,再更新数据库

先删除缓存,再更新数据库这种情况,如果并发量不大用起来不会有啥问题。但是在并发场景下会有这样的问题:线程A在删除缓存后,在写入数据库前发生了阻塞。这时线程B查询了这条数据,发现缓存中不存在,继而向数据库发起查询请求,并将查询结果缓存到了redis。当线程B执行完成后,线程A继续向下执行更新了数据库,那么这时缓存中的数据为旧数据,与数据库中的值不一致。

先更新数据库,再删除缓存

先更新数据库,再删除缓存也并不是绝对安全的,在高并发场景下,如果线程A查询一条在缓存中不存在的数据(这条数据有可能过期被删除了),查询数据库后在要将查询结果缓存到redis时发生了阻塞。这个时候线程B发起了更新请求,先更新了数据库,再次删除了缓存。当线程B执行成功后,线程A继续向下执行,将查询结果缓存到了redis中,那么此时缓存中的数据为A的数据,而数据库中却是B的数据,那么当A或B重新查询数据时,B会从缓存获得A更新的数据,而得不到数据库真实的数据,因此出现了数据不一致的情况。

解决数据不一致方案-延时双删

延时双删,即在写数据库之前删除一次,写完数据库后,再删除一次,在第二次删除时,并不是立即删除,而是等待一定时间在做删除。这种策略是分布式系统中数据库存储和缓存数据保持一致性的常用策略。

 当Redis中出现缓存数据一致性问题时,延时双删是一种常用的解决方案,而RabbitMQ可以通过消息的延迟特性来实现延时效果。下面我们来介绍一下如何实现:

首先我们需要创建一个RabbitMQ的交换机、队列和绑定关系,可以使用RabbitTemplate实现,如下所示:

@Configuration
public class RabbitConfig {
    private static final String EXCHANGE_NAME = "cacheExchange";
    private static final String DELAY_QUEUE_NAME = "cacheDelayQueue";
    private static final String ROUTING_KEY = "cacheKey";

    // 声明交换机
    @Bean
    public DirectExchange exchange() {
        return new DirectExchange(EXCHANGE_NAME);
    }

    // 声明延迟消息队列
    @Bean
    public Queue delayQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", EXCHANGE_NAME);
        args.put("x-dead-letter-routing-key", ROUTING_KEY);
        return new Queue(DELAY_QUEUE_NAME, true, false, false, args);
    }

    // 绑定延迟消息队列到交换机
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(delayQueue())
                .to(exchange())
                .with(ROUTING_KEY);
    }

    // 声明RabbitTemplate
    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setExchange(EXCHANGE_NAME);
        rabbitTemplate.setRoutingKey(ROUTING_KEY);
        return rabbitTemplate;
    }
}

在这里我们声明了一个名为cacheExchange的DirectExchange交换机和一个名为cacheDelayQueue的延迟队列。我们还绑定了延迟队列到交换机,并声明了一个RabbitTemplate,用于发送消息。

当我们需要清除缓存时,可以发送一个延迟消息到队列中。消息的时间间隔可以根据实际需求设置,这里以10秒为例:

@Autowired
private RabbitTemplate rabbitTemplate;

public void clearCache(String key) {
    // 删除Redis缓存中的数据
    redisTemplate.delete(key);
    // 发送延迟消息到队列中
    rabbitTemplate.convertAndSend(key, new Object(), message -> {
        message.getMessageProperties().setExpiration("10000");
        return message;
    });
}

在这里,我们先使用RedisTemplate删除缓存中的数据,然后使用RabbitTemplate发送延迟消息,将键值作为消息的key,这样我们就可以在消息处理程序中根据key来删除数据。在消息发送时,我们设置了消息的过期时间为10秒,这意味着消息将在10秒后到期并被发送到绑定的交换机。

最后,我们需要编写一个消息处理程序来处理延迟消息并进行双重删除操作。在这个示例中,我们简单地使用RedisTemplate删除缓存数据:

@Component
public class CacheMessageReceiver {

    @Autowired
    private RedisTemplate redisTemplate;

    @RabbitListener(queues = "cacheDelayQueue")
    public void onMessage(Message message) {
        String key = new String(message.getBody());
        // 双重删除
        redisTemplate.delete(key);
    }
}

在这个处理程序中,我们使用@RabbitListenercacheDelayQueue队列作为监听目标,当队列中有消息到达时,我们提取出消息的key,并使用RedisTemplate进行双删除操作,以确保缓存数据一致性得到解决。

总之,在Redis缓存中出现缓存数据一致性问题时,RabbitMQ的延迟消息机制可以帮助我们实现延时双删的效果,保证数据的一致性,并且也可以很方便地集成到我们的项目中。但它不是强一致。其实不管哪种方案,都避免不了Redis存在脏数据的问题,只能减轻这个问题,要想彻底解决,得要用到同步锁和对应的业务逻辑层面解决。

总结

在日常的使用中缓存确实能帮助数据库节省很多访问压力,但是在实际使用中确实有很多需要我们格外注意的地方,如果处理不好就容易造成数据不一致的情况,严重甚至导致服务宕机。因此如果在读多写少并且对数据一致性要求不严的情况使用基本是没啥大问题的。但是如果在高并发的场景下,还有很多坑是需要留心注意。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/491903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

企业组织管理神器:红海云可视化组织管理功能深度解析

在当前的VUCA时代&#xff0c;企业需要保持敏捷以应对变革和不确定性。组织架构作为承载战略目标的重要工具&#xff0c;如果无法敏捷调整&#xff0c;会直接影响企业战略的成功落地。但组织架构的设计和调整会触及其他业务&#xff0c;包括岗位、编制、人员与汇报关系等信息变…

优先级队列(大根堆与小根堆)

优先级队列&#xff08;大根堆与小根堆&#xff09; 文章目录 优先级队列&#xff08;大根堆与小根堆&#xff09;堆的介绍模拟堆以数组模型为例&#xff0c;创建堆向下调整&#xff08;shiftDown&#xff09;入队&#xff08;push&#xff09;及向上调整&#xff08;shiftUp&a…

java获取文件夹下所有文件名

在进行 Java编程的过程中&#xff0c;我们会经常使用到文件夹下的所有文件名。有时候可能不太熟悉 Java编程的小伙伴们会发现&#xff0c;在代码中没有获取到所有的文件名&#xff0c;那么这个时候我们应该怎么去获取到这些文件呢&#xff1f;在进行 Java编程的过程中&#xff…

《花雕学AI》31:ChatGPT--用关键词/咒语/提示词Prompt激发AI绘画的无限创意!

你有没有想过用AI来画画&#xff1f;ChatGPT是一款基于GPT-3的聊天模式的AI绘画工具&#xff0c;它可以根据你输入的关键词/咒语/提示词Prompt来生成不同风格和主题的画作。Prompt是一些简短的文字&#xff0c;可以用来指导ChatGPT的创作过程。在这篇文章中&#xff0c;我将展示…

2个月快速通过PMP证书的经验分享

01 PMP证书是什么&#xff1f; 指的是项目管理专业人士资格认证。它是由美国项目管理协会&#xff08;Project Management Institute(简称PMI)&#xff09;发起的&#xff0c;严格评估项目管理人员知识技能是否具有高品质的资格认证考试。其目的是为了给项目管理人员提供统一的…

Redis【性能 02】Redis-5.0.14伪集群和Docker集群搭建及延迟和性能测试(均无法提升性能)

伪集群及Docker集群搭建测试流程 1.伪集群搭建1.1 环境1.2 搭建1.2.1 集群配置1.2.2 生成其他5个节点配置1.2.3 启动并验证节点状态1.2.4 创建集群1.2.5 集群信息 1.3 测试 2.Docker集群2.1 环境2.2 搭建2.2.1 创建专用网络2.2.2 生成配置文件2.2.3 容器启动及验证2.2.4 创建集…

NIST SP 800-193: BIOS 平台固件弹性指南

NIST SP 800-147&#xff0c;BIOS 保护指南 ( NIST SP 800-147 [1]、NIST SP 800-147B [2]&#xff09;解决了 BIOS 的保护问题 可从此处免费获得&#xff1a; https://doi.org/10.6028/NIST.SP.800-193 摘要 此文档提供了关于支持平台固件和数据对抗潜在地具有破坏性的攻…

python的 __init__.py文件中使用__all__变量

在Python的包&#xff08;Package&#xff09;中&#xff0c;init.py文件可以被用作初始化包的脚本。这个文件会在包被导入时自动执行。同时&#xff0c;init.py文件中的__all__变量也可以被用来限制包中可导入的模块、类或方法。具体来说&#xff0c;__all__变量应该是一个列表…

项目上线 | 兰精携手盖雅工场,数智驱动绿色转型

近年来&#xff0c;纺织纤维行业零碳行动如火如荼。作为低碳环保消费新时尚引领者&#xff0c;同时也是纤维领域隐形冠军&#xff0c;兰精在推进绿色发展的同时&#xff0c;也在不断向内探索企业数字化转型之道&#xff0c;以此反哺业务快速扩张。 数智转型&#xff0c;管理先…

计算机网络面试题(上)

1.TCP/IP 网络模型有哪几层&#xff1f; TCP/IP 网络通常是由上到下分成 4 层&#xff0c;分别是应用层&#xff0c;传输层&#xff0c;网络层和网络接口层。 每一层的封装格式&#xff1a; 网络接口层的传输单位是帧&#xff08;frame&#xff09;&#xff0c;IP 层的传输单位…

探究肺癌患者的CT图像的图像特征并构建一个诊断模型

目标效果图操作说明代码 目标 探究肺癌患者的CT图像的图像特征并构建一个诊断模型 效果图 操作说明 代码中我以建立10张图为例&#xff0c;多少你自己定 准备工作&#xff1a; 1.准备肺癌或非肺癌每个各10张图&#xff0c;在本地创建一个名为“data”的文件夹&#xff0c;用…

【Docker】什么是Dockerfile

文章目录 1、认识DockerFile2、DockerFile的构建过程3、DockerFile常用指令4、实战&#xff1a;构建自己的centos5、CMD和ENTRYPOINT的区别6、DockerFile制作tomcat镜像7、发布镜像到DockerHub8、发布镜像到阿里云 1、认识DockerFile Dockerfile是用来构建docker镜像的文件&am…

arduino stm32 开发环境 解决方案

用到工具 hfs.exe 做文件服务器 来模拟所有需要下载的文件 https://download.csdn.net/download/qq_32562225/87754346 其原理就相当于 本应arduinoIDE 下载的文件&#xff0c;先手动通过迅雷工具下载下来&#xff0c;然后再添加到文件服务器中&#xff0c;这样就可以快速…

爬虫想要的HTML

我的个人博客主页&#xff1a;如果’真能转义1️⃣说1️⃣的博客主页 关于Python基本语法学习---->可以参考我的这篇博客&#xff1a;《我在VScode学Python》 接下来回更新一个关于urllib的文章 爬虫一个新浪博客地址 import urllib.requestpage 1 url [" "] *…

如何在线录制视频?教您一个简单的方法!

案例&#xff1a;怎样实现在线录屏&#xff1f; 【听朋友说在线录屏更加便捷&#xff0c;我也想学习如何在线录制电脑屏幕。有没有小伙伴有在线录屏的经验&#xff0c;求好心人给一个简单的方法&#xff01;】 在今天的数字时代&#xff0c;我们经常需要录制电脑屏幕来制作教…

【C++】AVL树的插入实现(详解旋转机制)

✍作者&#xff1a;阿润菜菜 &#x1f4d6;专栏&#xff1a;C 文章目录 AVL树的定义AVL树的旋转机制1.左旋操作 --- 新节点插入较高右子树的右侧---右右&#xff1a;左单旋2.右旋操作 --- 新节点插入较高左子树的左侧——左左&#xff1a;右单旋3.左右双旋 --- 新节点插入较高左…

JMeter开发自动化接口测试脚本练习

一、打开浏览器代理服务器设置 我这里用的是360浏览器&#xff0c;打开浏览器代理服务器设置&#xff0c;端口要与jmeter中的端口设置保持一致哦。 二、JMeter设置代理 JMeter设置代理&#xff08;jmeter中的端口要与360浏览器端口设置保持一致哦。&#xff09; 三、启动代理运…

BM6 判断链表中是否有环

判断链表中是否有环_牛客题霸_牛客网 (nowcoder.com) 双指针&#xff0c;快指针一次走两步&#xff0c;慢指针一次走一步&#xff0c;快指针不为空且快指针的下一个指针不为空的情况下 若快慢指针相遇即位有环。 /** * Definition for singly-linked list. * struct ListNode {…

基于RK3588的8K智能摄像机方案设计

设计了一款基于石墨烯散热的8 K智能摄像头&#xff0c;主控采用瑞芯微RK3588&#xff0c;传感器采用索尼IMX435&#xff0c; 通过HDMI2.1将传感器采集到的图像发送到8 K显示器&#xff0c;实现端到端的8 K呈现&#xff0c;为了确保摄像头性能稳定&#xff0c;本 设计采用石墨烯…

ETL到底是什么?

各位数据的朋友&#xff0c;大家好&#xff0c;我是老周道数据&#xff0c;和你一起&#xff0c;用常人思维数据分析&#xff0c;通过数据讲故事。 前段时间和大家聊了一个话题&#xff0c;就是为什么要用构建数据仓库&#xff0c;而不是直连数据源的方式开发报表&#xff1f;通…