淘先锋技术网

首页 1 2 3 4 5 6 7

学习目标

  Redis简介
  Redis的安装使用
  Redis的数据类型
  SpringBoot整合Redis
  声明式缓存
  Redis的常见问题
  事务
  分布式锁
  持久化策略
  淘汰策略

Redis简介

  Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

特点:

  1. Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  2. Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  3. Redis支持数据的备份,即master-slave模式的数据备份。

Redis的优势:

  1. 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  2. 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Set 及Zset 数据类型操作。
  3. 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  4. 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis的安装使用

Linux安装

  • 先安装gcc编译器,可以用来编译c、c++等代码

     yum -y install gcc //-y表示自动安装
    
  • 安装Redis

     wget http://download.redis.io/releases/redis-3.2.5.tar.gz //下载redis安装包到usr/local
     tar xzf redis-2.8.17.tar.gz
     cd redis-2.8.17
     make
    
  • Redis的配置(进入到redis目录下的redis.conf文件)

     cd /usr/local/redis-5.0.4
     vi redis.conf(本人的redis放在/usr/local)
    
  • 注释掉 bind 127.0.0.1

  • 关闭保护模式 protected mode no
    在这里插入图片描述

  • 使用Redis

  • 1、启动服务器 src中 ./redis-server …/redis.conf
    在这里插入图片描述

  • 2、启动客户端 src中 ./redis-cli
    在这里插入图片描述

Windows安装

  • 从官网下载redis的windows版本
  • 解压
  • 启动服务器端
  • 启动客户端
    在这里插入图片描述

Redis的数据类型

数据类型有:

  • string 字符串(适合保存单个数据)

     	set key value
     	get key
     	可以设置失效时间
     	set key value EX 10  //10秒钟之后失效
    

在这里插入图片描述

  • hash 哈希(适合保存复杂类型数据,如:一般可以用来存储Java中的一个完整的自定义对象)

     	//hmset是存储hash值的指令,
     	//user是当前hash的key
     	//name "zhangsan" age 23 sex "nan" 是 key对应的值
     	127.0.0.1:6379> hmset user name "zhangsan" age 23 sex "nan"
     	OK
     	//hmget获取hash中的某一个属性的值
     	127.0.0.1:6379> hmget user name
     	1) "zhangsan"
     	127.0.0.1:6379> hmget user age
     	1) "23"
     	//hgetall是获取hash中的所有属性对应的值
     	127.0.0.1:6379> hgetall user
     	1) "name"
     	2) "zhangsan"
     	3) "age"
     	4) "23"
     	5) "sex"
     	6) "nan"
    
  • list 列表(适合保存有序的、可重复的数据)

    采用的链表结构进行数据存储

    lpush 从右向左添加

    rpush 从左向右添加

    lrange key start stop

     //lpush用来存储一个列表的命令。interesting是列表的名称,"basketball"列表中的值
     127.0.0.1:6379> lpush interesting "basketball"
     (integer) 1
     127.0.0.1:6379> lpush interesting "football" "ball"
     (integer) 3
     //lrange输出列表中的数据的命令, interesting就是列表的名称 。 0 2是列表的开始输出索引和结束索引。
     127.0.0.1:6379> lrange interesting 0 2
     1) "ball"
     2) "football"
     3) "basketball"
    
  • set 无序集合(适合保存无序的,不可重复的数据)

     sadd key member //存数据
     smembers key //取数据
     案例:
     127.0.0.1:6379> sadd strset "a" "b" "c"
     (integer) 3
     127.0.0.1:6379> smembers strset
     1) "b"
     2) "c"
     3) "a"
    
  • zset 有序集合(适合保存有序的,不可重复的数据)

     zadd key score member  (score是一个数字,zset就是通过这个数字进行排序,可以重复)
     zrangebyscore key 0 1000 //通过分数排序输出
     有序集合是按照score进行排序
    

在这里插入图片描述

SpringBoot整合Redis

1)依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2)配置文件

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-wait=100ms
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=10

3)配置RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(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);
        template.afterPropertiesSet();
        return template;
    }
}

4)使用RestTemplate
常用方法:

  1. opsForValue 获得string类型的操作对象
  2. opsForHash 获得hash类型的操作对象

5) 缓存使用的流程

按id查询商品的过程

1) 以id为键查询Redis缓存,如果能查到就返回数据,结束

2)如果查不到,就查询数据库,数据库查到,缓存到Redis,返回数据

3)如果数据库查不到,返回null,结束

4)增删改数据库的同时,要修改缓存

@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {

    public static final String TYPE = "GOODS-";

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    public Goods getGoodsById(Long id){
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        //1) 以id为键查询Redis缓存,如果能查到就返回数据,结束
        Object value = ops.get(TYPE + id);
        //2)如果查不到,就查询数据库
        if(value == null){
            System.out.println("缓存不存在,查询数据库");
           // 数据库查到,缓存到Redis,返回
           Goods goods = this.getById(id);
           if(goods != null){
               System.out.println("数据库存在,保存到缓存");
               ops.set(TYPE + id,goods);
           }else{
               System.out.println("数据库不存在,返回null");
           }
           return goods;
        }else{
            System.out.println("缓存存在,返回"+value);
           //如果能查到就返回数据,结束
            return (Goods) value;
        }
    }
}

声明式缓存

SpringBoot项目需要的依赖,配置文件同上

1.在启动类上添加注解 @EnableCaching
2.Redis的配置类

@Configuration
public class RedisConfig {

    @Bean
    public RedisCacheConfiguration provideRedisCacheConfiguration(){
        //加载默认配置
        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig();
        //返回Jackson序列化器
        return conf.serializeValuesWith(
                RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

3)缓存注解

  • @CacheConfig 使用在Service类上,如:@CacheConfig(cacheNames = “books”)

  • @Cacheable 使用在查询方法上,让方法优先查询缓存

  • @CachePut 使用在更新和添加方法上,数据库更新和插入数据后同时保存到缓存里

  • @CacheEvict 使用在删除方法上,数据库删除后同时删除缓存

  • @Caching使用在删除方法上里面可以装多个@CacheEvict,数据库删除后同时删除缓存

注意:实体类必须实现序列化接口

@CacheConfig(cacheNames = "GOOD")
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goodss> implements IGoodService {
    @Autowired
    private GoodsMapper goodsMapper;

    @Cacheable(key = "T(String).valueOf(#id)")
    @Override
    public Goodss selectOneGoods(Long id) {
        return goodsMapper.selectOneGoods(id);
    }

    @Cacheable(cacheNames = "goods-page",key = "T(String).valueOf(#page.current)")
    @Override
    public IPage<Goodss> getSelectGoodss(IPage<Goodss> page) {
        return goodsMapper.getSelectGoodss(page);
    }

    @CachePut(key="T(String).valueOf(#goodss.id)")
    @CacheEvict(cacheNames = "goods-page",allEntries = true)
    @Override
    public Goodss saveGoods(Goodss goodss) {
         this.save(goodss);
         return goodss;
    }

    @CachePut(key="T(String).valueOf(#goodss.id)")
    @CacheEvict(cacheNames = "goods-page",allEntries = true)
    @Override
    public Goodss updateGoods(Goodss goodss) {
         this.updateById(goodss);
        return goodss;
    }


    @Caching(evict = {@CacheEvict (key = "T(String).valueOf(#id)"),@CacheEvict(cacheNames = "goods-page",allEntries = true)})
    @Override
    public void deleteGoods(Long id) {
        this.removeById(id);
    }

}

Redis的常见问题

1)缓存击穿

高并发的情况下,短时间内缓存会被穿过,请求直接打到数据库上,可能导致数据库压力过大。

解决方案:对代码上锁(双重检查锁)

2)缓存穿透

高并发的情况下,如果查询不存在的数据,因为缓存和数据库都不存在,请求都会打到数据库上,可能导致系统崩溃。

解决方案:

​ 1) 保存不存在的数据到缓存中,设置一定过期时间

​ 2) 布隆过滤器(直接过滤掉不存在数据的请求) 不能准确判断是否存在数据,能准确判断数据不存在

3)缓存雪崩

高并发的情况下,缓存服务器重启或热点数据同时过期,全部访问数据库,导致数据库宕机

解决方案:

​ 1)配置缓存集群

​ 2)尽量给热点数据设置不一样的过期时间,相对均匀

解决代码

 public Goods getGoodsById(Long id){
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        Object value = ops.get(TYPE + id);
        //外层先读缓存,缓存如果有,就不执行同步块
        if(value == null) {
            synchronized (this) {
                //1) 以id为键查询Redis缓存,如果能查到就返回数据,结束
                value = ops.get(TYPE + id);
                //2)如果查不到,就查询数据库
                if (value == null) {
                    System.out.println("缓存不存在,查询数据库");
                    // 数据库查到,缓存到Redis,返回
                    Goods goods = this.getById(id);
                    if (goods != null) {
                        System.out.println("数据库存在,保存到缓存");
                        ops.set(TYPE + id, goods);
                    } else {
                        System.out.println("数据库不存在,返回null");
                        //保存空数据到缓存中,设置过期时间
                        ops.set(TYPE + id,new Goods(),30, TimeUnit.SECONDS);
                    }
                    return goods;
                } else {
                    System.out.println("缓存存在,返回" + value);
                    //如果能查到就返回数据,结束
                    return (Goods) value;
                }
            }
        }
        System.out.println("缓存存在,返回" + value);
        return (Goods) value;
    }

事务

Redis的事务是将一系列操作打包,一起提交。没有原子性,隔离性的,也没有回滚事务。

multi 启动事务
在这里插入图片描述

exec 提交事务
在这里插入图片描述

discard 放弃事务

watch 监视某个数据,如果修改该数据时,在另一个事务中对该数据进行了修改,当前的修改就被放弃
在这里插入图片描述

总结:

事务中如果出现语法错误,整个事务无法执行;如果出现数据错误,事务可以成功一部分,失败一部分。

PS:incr 增加数值,decr 减少数值

分布式锁

Redis可以作为分布式锁,让所有服务共同访问,内部的数据可以使用watch(乐观锁)机制进行监控。

悲观锁能保证线程安全,但是性能低

Redis提供乐观锁,性能较高

Redis内部是单线程,不存在线程安全问题

案例:模拟秒杀功能,商品库存10,1000用户来抢,不能出现超买的情况

@Repository
public class GoodsCache {

	Logger logger = Logger.getLogger(GoodsCache.class);
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

public String buy(){
    Boolean execute = redisTemplate.execute(new SessionCallback<Boolean>() {
        @Override
        public Boolean execute(RedisOperations redisOperations) throws DataAccessException {
            //开启乐观锁,当前线程执行exec释放锁之前,其他线程进行修改都会失败
            redisOperations.watch("goods_num");
            //读取商品数量
            int num = Integer.parseInt(redisOperations.opsForValue().get("goods_num").toString());
            if (num <= 0) {
                logger.error("已售完");
                return false;
            }
            //开启事务
            redisOperations.multi();
            //修改商品数量
            redisOperations.opsForValue().increment("goods_num", -1);
            //提交事务,释放锁
            List list = redisOperations.exec();
            //如果事务执行失败,返回的集合中数据为空
            if (list == null || list.isEmpty()) {
                logger.error("购买失败");
                return false;
            }
            logger.error("购买成功" + list.get(0));
            return true;
        }
    });
    return String.valueOf(execute);
}
}

@Controller
@RequestMapping("/products")
public class ProductController {
    @ResponseBody
    @RequestMapping("/buy")
    public JSONResult buy(){
        return new JSONResult(1,goodsService.buy());
    }
}

持久化策略

为什么持久化?Redis属于内存式数据库,程序关闭后数据会清空,有时候需要将内存中的数据长期在文件保存起来

持久化策略

AOF:默认每秒对数据进行持久化

RDB:按条件触发持久化操作(任意一个)

900 1 900秒中修改1次

300 10 300秒中修改10次

60 10000 60秒中修改10000次

配置方法

RDB

在这里插入图片描述

AOF

appendonly yes / no yes开启AOF

appendfsync everysec 每秒保存

如何选择?

允许少量数据丢失,性能比较高----RDB

只允许很少数据丢失----AOF

不允许数据丢失----RDB + AOF

淘汰策略

为什么要淘汰?Redis数据保存在内存中,数据太多会出现溢出问题,Redis会根据某些策略淘汰一些数据

64位系统,上限就是内存上限;32位最大4G

配置最大内存:

max-memory 配置0就是无上限(默认)

LRU算法:Least Recently Used 最近最少使用算法,淘汰长期不用的缓存

淘汰策略:

maxmemory-policy

值:

noevication(默认) 不淘汰

allkeys-lru(推荐) 使用LRU淘汰比较少使用的键

volatile-lru 在过期的键中淘汰较少使用的

allkeys-random 在所有键中随机淘汰

volatile-random 在过期键中随机淘汰

volatile-ttl 在过期键中淘汰存活时间短的键