Spring boot的基础总结(三)-------Redis

上回讲到了基本校验和统一参数返回.
是不是以为我这章要记录mybatis啊?~
不好意思,这章记录redis.
在这里插入图片描述
因为现在数据库那部分不是我做的,哈哈哈~
言回正传.

5.Redis

5.1 Redis是什么,为什么要用?
Redis是一种非关系型数据库,用key-value存储数据。
不同于MYSQL之类的关系型数据库,有字段,属性,实体之类的概念,但是Redis没有,我大概是把它看做成Java的Map来看待的。不过是一个超无敌无敌好用的HashMap.
为什么要用,这问题就牵扯到它的特性了.

速度快:由于Redis是将数据放缓存,所以它每次拿数据并不用

像MySQL一样去磁盘里面去拿,在查询数据上,也就快很多。所以很多时会拿来做缓存中间件
原子性:由于操作是原子性,要么做就做完,要么不做完。很多操作可以不考虑线程安全问题所以很适合数据库。

那么它最大的作用是什么呢!减少数据库压力!!!你想想,如果每一个请求都是对数据库进行查询,数据库累的得死,再猛也要给喘口气啊。那就想了,我能不能查一次,然后存在一个地方,用的时候就去哪里取,等要更新的时候再到数据库取。
这就是redis了!而且它里面的数据类型很多,无论java,python啥的都能用。如果还是不能理解它是一个什么东西,先去看一下关系型数据库和非关系型数据库。

5.2 配置Spring boot的redis

  1. 事前准备
    和网上大多数的教程一样,但是很多人不写版本,让人搞来搞去很难受。这里就我自己实践的将自己的一些理解记录下来.
    先安装Redis
    第一节去下载吧,都一样
    然后去到自己windows服务里面,看下Redis有没有开,我第一次是没有开启,然后手动开的,然后设为自动启动。
    在这里插入图片描述
    然后安装Redis Desktop Manager
    但是现在新版本要收费了!所以去搜一个旧版本然后不升级就好了,先用着吧。
    我们偷偷摸摸就好了,别告诉别人

为什么要安装,因为你机子没有!这是一个服务器~!spring boot 可没有义务帮你!

  1. 配置spring boot
    添加依赖
    加入灵魂~
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

添加配置文件,这里网上很多教程分不清spring boot 1.*版本和2.*版本,我这里用的2.*版本,就直接在properties文件里面配置就完成了!

#Redis的服务器地址
spring.redis.host=localhost
#Redis服务器连接端口
spring.redis.port=6379
# Redis数据库索引(默认为 0 )
spring.redis.database= 0
spring.redis.database1= 1
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000

  1. 基本案例
    然后我们新建类,继承CachingConfigureSupport用来配置我们的redis实例。在存入的时候要记得序列化里面的值,否则将会很在可视化里面基本都是乱码,很难看里面到底有什么东西。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
/**
     * 自定义生成redis-key
     *
     * @return
     */
    @Bean
    public KeyGenerator wiselyKeyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }
}
 @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))          .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
    
 @Autowired
    private LettuceConnectionFactory factory;

    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate() {
        // 关闭共享链接
        factory.setShareNativeConnection(false);

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        setSerializer(redisTemplate);

        redisTemplate.setConnectionFactory(factory);

    return redisTemplate;
    }
 private void setSerializer(RedisTemplate<String, Object> template) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //指定序列化类型,如果不指定就是纯json,java解析就是一个LinkHashMap类型的key-value类型
        //指定了就会自动的转化为ArrayList和model,app
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key也采用String序列化
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
    }

这里就相当于我们将整个redis配置就弄完了,毕竟我们不会再每个代码里面去重复放入取出操作,这里我们用工具类即可了。

@Component
public class RedisCache  {

    private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();


    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> gredisTemplate;

    @Autowired
    @Qualifier("redisTemplateTwo")
    private RedisTemplate<String, Object> gredisTemplate_second;

    //这个缓存时间最好用一定时间内的随机数,防止缓存雪崩
    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间

    //返回此缓存将指定键映射到的值,
    //*通常指定返回值将被强制转换为的类型。
    public <T> T get(Object key, Class<T> type) {
        T Return;
        RedisTemplate redisTemplate = gredisTemplate;
        ValueOperations opsForValue = redisTemplate.opsForValue();
        Return = (T)opsForValue.get(key);
        return Return;
    }

    public Object get(String key){
        RedisTemplate redisTemplate = gredisTemplate;

        if(key == null){
            return null;
        }else{
            return redisTemplate.opsForValue().get(key);
        }

    }


    public void put(String key, Object value) {
        RedisTemplate redisTemplate = gredisTemplate;
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
    }

    //从缓存中删除对应的key

    public void evict(String key) {
        RedisTemplate redisTemplate = gredisTemplate;
        redisTemplate.delete(key);
        logger.debug("Remove cached query result from redis");
    }

    public void evict(String... key) {
        RedisTemplate redisTemplate = gredisTemplate;
        if(key !=null &&key.length>0){
            if(key.length ==1){
                redisTemplate.delete(key);
            }else{
                redisTemplate.delete(CollectionUtils.arrayToList(key));

            }
        }
        logger.debug("Remove cached query result from redis");
    }
    //发生更新时清楚缓存

//    public void clear() {
//        RedisTemplate redisTemplate = gredisTemplate;
//        redisTemplate.execute((RedisCallback) connection -> {
//            connection.flushDb();
//            return null;
//        });
//        logger.debug("Clear all the cached query result from redis");
//    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间() 返回0代表为永久有效
     */
    public long getExpire(String key){
        return gredisTemplate.getExpire(key,TimeUnit.SECONDS);
    }

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间()
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                gredisTemplate.expire(key, time, TimeUnit.MINUTES);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 判断key存不存在
     * @param key 键
     * @return return即存在
     */
    public boolean HasKey(String key){
        boolean ReturnBoolean = false;
        try{
            ReturnBoolean = gredisTemplate.hasKey(key);
        }catch (Exception e){
            e.printStackTrace();

        }
        return ReturnBoolean;
    }


    /**
     * 哈希 添加
     * @param key
     * @param hashKey
     * @param value
     */
    public void hmSet(String key, Object hashKey, Object value){
        HashOperations<String, Object, Object> hash = gredisTemplate.opsForHash();
        hash.put(key,hashKey,value);
        expire(key,EXPIRE_TIME_IN_MINUTES);
    }

    /**
     * 哈希获取数据
     * @param key
     * @param hashKey
     * @return
     */
    public Object hmGet(String key, Object hashKey){
        HashOperations<String, Object, Object>  hash = gredisTemplate.opsForHash();
        return hash.get(key,hashKey);
    }
    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        gredisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 列表添加
     * @param k
     * @param v
     */
    public void lPush(String k,Object v){
        ListOperations<String, Object> list = gredisTemplate.opsForList();
        list.rightPush(k,v);
    }
    /**
     * 列表获取
     * @param k
     * @param l
     * @param l1
     * @return
     */
    public List<Object> lRange(String k, long l, long l1){
        ListOperations<String, Object> list = gredisTemplate.opsForList();
        return list.range(k,l,l1);
    }


    public void ChangeDb(int db){
        //这个是共享连接的
//        RedisConnection redisConnection = gredisTemplate.getConnectionFactory().getConnection();
//        DefaultStringRedisConnection stringRedisConnection = new DefaultStringRedisConnection(redisConnection);
//        stringRedisConnection.select(db);
        //改成lettuceConnectionFactory,
//        LettuceConnectionFactory jedisConnectionFactory = (LettuceConnectionFactory) gredisTemplate
//                .getConnectionFactory();
//        jedisConnectionFactory.setDatabase(db);//这个好像线程不安全噢
//        jedisConnectionFactory.resetConnection();
//        gredisTemplate.setConnectionFactory(jedisConnectionFactory);

        RedisTemplate redisTemplate = gredisTemplate_second;
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set("key2", 1000, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
    }
}

当然这些工具类只是一部分,大家也可以自己加油扩展。

  1. 分库redis
    OK,这里我们能看到在配置文件中,我们的配置数据库的索引默认是0.而redis默认有16个db,索引从0-15.如果我们不改变配置,那么存的东西一直都在db0中,对于小系统可能没有什么问题,但是如果系统大了,把所有东西都存在db0中,我总感觉有点不对劲,所以如何将数据存在db1-15中也重要。

在这里插入图片描述
在这里,我们先稍微想一想,存入数据操作你会放在主线程吗,如果所有操作放在主线程,那么可想而知如果用户量开始稍微大一点,你的服务器就负担很大了,所以存入写入基本操作都在线程中。那我们代码中切换所存入的db,产生的问题就是要加锁,因为redis切库本身是线程不安全的,那我们换一种思路。产生多个db,redis实例,每个实例对应一个Base不就完了嘛~在我们配置类中,多加一个Bean.
SpringBoot 2.x 之后连接redis驱动默认使用的是lettuce,而非之前的jedis。很多教程现在还是用的jedis,所以坑还是稍微有点多的0 0.不过这里我有一点不明白的是,如果我直接自动注入为什么不可以切换实例的数据库~
先加上依赖

   <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

加上配置代码

  /**
     * 第二个redis
     * @return
     */
    @Bean(name = "redisTemplateTwo")
    public RedisTemplate<String, Object> redisTemplateTwo() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        setSerializer(redisTemplate);
        redisTemplate.setConnectionFactory(getStringRedisTemplate(1));
        return redisTemplate;
    }


    @Bean
    public GenericObjectPoolConfig getPoolConfig(){
        // 配置redis连接池
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxWaitMillis(maxWait);
        return poolConfig;
    }

    private LettuceConnectionFactory getStringRedisTemplate(int database) {
        // 构建工厂对象
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
        configuration.setPassword(RedisPassword.of(password));
        LettucePoolingClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
                .commandTimeout(Duration.ofSeconds(timeout)).poolConfig(getPoolConfig()).build();
        LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration, clientConfiguration);
        // 设置使用的redis数据库
        factory.setDatabase(database);
        // 重新初始化工厂
        factory.afterPropertiesSet();
        return factory;
    }

这里因为在同一个配置类中的Bean有很多,spring会分不清喔,所以要加上Name.然后再setDatabase()函数中去设置自己想去存储的db即可。那工具类就需要定义好不同的实例,这样不同的实例就会存储到不同的db了

   @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> gredisTemplate;

    @Autowired
    @Qualifier("redisTemplateTwo")
    private RedisTemplate<String, Object> gredisTemplate_second;

  public void put(String key, Object value) {
        RedisTemplate redisTemplate = gredisTemplate;
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
    }
  public void put2(String key, Object value) {
        RedisTemplate redisTemplate = gredisTemplate_second;
        ValueOperations opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
    }

结尾

好了,这里先总结到这里。
Spring boot的redis功能主要是能减少很多数据库的压力。所以这个功能点还是很有用而且需要的。
当然redis还需要很多解决的问题,我这里自己记录一下,继续有时间的话可以继续解决这些问题。

缓存倾斜
集群模式
主从机制
哨兵(烧饼)模式


新的一年,新的开始

回来填坑了,打工是真的困又累。干巴爹,打工人!
缓存穿透:
缓存穿透是指查询一个一定不存在的数据,这样无论查询多少次,缓存就一定不存在,穿透就会不断的打在数据库上,这个时候缓存就失去了意义,在流量大的时候,甚至会让DB挂掉。
基于存在这种可能性,一般会有两种方案:缓存空值和布隆过滤器。

  • 缓存空值
    缓存空值就是在查询为空时,也将空值缓存下来,因为空值不是业务数据,也会占用缓存控件,所以设置一个较短的过期时间。伪代码如下:
Object nullValue = new Object();
try {
  Object valueFromDB = getFromDB(uid); //从数据库中查询数据
  if (valueFromDB == null) {
    cache.set(uid, nullValue, 10);   //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
  } else {
    cache.set(uid, valueFromDB, 1000);
  }
} catch(Exception e) {
  cache.set(uid, nullValue, 10);
}

这种方法比较简单粗暴,但是如果缓存系统有大量的空值,会浪费缓存的存储空间,如果都被这种空值占满了,还会使得一些真正有用的用户信息被剔除。

  • 布隆过滤器
    “你可以相信布隆~~”
    布隆过滤器底层是一个超级大的 bit 数组,默认值都是 0 ,一个元素通过多个hash函数映射到这个 bit 数组上,并且将 0 改成 1。
    虽然布隆过滤器存在一定的误判,不在数据库的元素也有可能被判断在布隆过滤器上,但是不在布隆过滤器中的元素一定不存在数据库中
    在服务启动的时候,先把数据查询条件,如数据ID映射到布隆过滤器上,看是否存在,如果不存在就返回控制了,存在的才继续查询数据库和缓存。这样就解决了缓存透传的问题了。应用问题我就不写了,不太会,不过可以去看看大佬的。
    可以说的是,把大量的数据已经打入布隆过滤器了,这样一些恶意透传就会被过滤掉 了。
    大佬的应用

缓存击穿:
缓存击穿(雪崩),雪崩和击穿都一个key或多个key的区别而已。是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大。解决方案如下:

  • 缓存失效时间分散
    在原有的失效时间基础上增加一个基础值,比如1-5分钟随机,这样每一个缓存的过期时间重复率会降低,集体灾难的几率也会降低。
  • 互斥锁
    当缓存失效的时候,不立即去数据库读取,而是先用Redis的setnx先去设置一个互斥锁,操作成功后进行数据库读取。伪代码如下
    在这里插入图片描述

还有一个热点数据永不过期的,我觉得不太合理,所以就在这里不介绍了。

缓存倾斜
缓存倾斜和大量请求数据库差不多,就是对某个key请求过量,使得缓存服务器挂掉了。只是缓存服务器挂掉了,跟数据库没啥关系

  • 客户端解决
    把热点key放到客户端存储,设置过期时间,我们解决不了就让客户端去解决吧。
  • 服务端解决
    把这个key复制出多个子key,value一样,查询的时候用hash取模分配到不同的子key去。分摊压力,
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值