redis学习

发布时间:2024年01月19日

redis

存取数据时,是键值对的形式key-value

  • 比如存一个用户信息时,key = 1001,value 可以为 用户的信息,是用json数据存的
SQLNoSQL
结构化:(修改起来很麻烦)非结构化(健值,文档型,列表,Graph)
关系型的,需要维护无关联
sql查询非sql
ACID(原子性,一致性,隔离性,持久性)BASE
磁盘存储内存存储(速度快)
垂直水平

1、特征

  • 键值型:value支持多种不用的数据结构

  • 单线程,每个命令具有原子性

  • 低延迟,速度快(基于内存,io多路复用,良好的编码)

  • 支持数据持久化,定期存磁盘中

  • 支持主从集群,分片集群

  • 支持多语言客户端

2、数据结构类型

2.1、值的类型:

  • 基本类型:不能重复

    • string

    • hash

    • list

    • set

    • sortedSet

  • 特殊类型

    • GEO

    • Bitmap

    • Hyperlog

  • 还有些其他类型

2.2、redis的通用命令

  • keys 模糊查询

    • keys a* 查询以 a 开头的键
  • del 删除命令

    • del name 删除键为 name 的数据
  • exists 判断key是否存在

    • exists name 返回值为key 的数量
  • expire 设置有效期

    • expire 键 存活时间 expire name 20,存活20秒
  • ttl 查看key剩余的有效期

    • ttl name

2.3、string类型

2.3.1、存储格式
  • 字符串格式分为三种,底层是字节数组形式存储
    • string:普通字符串
    • int:整型类型 ,可以自增自减
    • float:浮点类型,可以自增自减
    • image-20240117152428673
2.3.2、string的内部实现
  • string底层采用的数据结构是:int和sds(简单字符串)
  • redis是C语言编写的,并没有使用C语言的字符串因为sds有以下优点
    • sds不仅可以保存文本数据,还可以保存二进制数据,不仅可以保存文本,还能存图片等等的二进制数据。
    • sds获取字符串长度的复杂度是O(1),因为在sds结构中有 len来记录长度,直接可以获取,所以为O(1)。
    • redis的sdsAPI是安全的,拼接字符串不会造成缓冲区溢出,因为sds会检查空间大小,不够时会自动扩容,所以不会造成缓冲区溢出的问题。
2.3.3、常见的命令
  • 基本操作

    • set :添加或者修改

    • get:获取值

    • exists:判断key是否存在

    • strlen:获取长度

  • 批量设置

    • mset:批量添加
    • mget:批量获取
  • 计数器

    • incr :自增+1

    • incrby:设置自增的步长

    • derc:自减-1

    • decby:设置自减的步长

    • incrbyfloat :必须设置指定增长的步长

2.3.4、层级存储
  • 项目名:业务名:类型:id ===》heima:user:1
  • 例如value是java对象
    • heima:user:1 -----> {“id”: 1,“name” : “jack”,“age” : 21 }
    • heima:user:1 -----> {“id”: 1,“name” : “小米手机”,“price” : 3999 }
  • image-20240117145245199
2.3.5、应用场景
  • 缓存对象

    • 直接缓存整个JSON,例如:SET user:1 {“name”:“zzj”,“age”:18},同时也采用了分层思想
    • 采用key进行分离为 user:ID 属性,采用 mset储存,用mget 获取各属性值,例如:MSET user:1:name xiaoming user:1:age 18 user:2:name xiaowang user:2:age:20
  • 常规计数

    • 以为redis处理命令是单线程的,所以执行命令的过程是原子性的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。

    • # 初始化文章的阅读量
      > SET aritcle:readcount:1001 0
      OK
      #阅读量+1
      > INCR aritcle:readcount:1001
      (integer) 1
      #阅读量+1
      > INCR aritcle:readcount:1001
      (integer) 2
      #阅读量+1
      > INCR aritcle:readcount:1001
      (integer) 3
      # 获取对应文章的阅读量
      > GET aritcle:readcount:1001
      "3"
      
  • 分布式锁

    • SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:

2.4、hash类型

2.4.1、介绍
  • key和string没啥变化,主要是value是一个无序的字典,是松散的

  • hash结构将,String中的json变成键值类型

  • keyfieldvalue
    heima:user:1namejack
    age21
2.4.2、基本命令
  • HSET:添加操作,要指定三个参数,例如:HSET user:3 name xiaozeng
  • HGET:读取操作,例如:HGET user:3 age

  • HMSET:批量添加,例如:HMSET user:3 name xiaoli age 18 sex nan phone 123456789
  • HMGET:批量读取,例如:HMGET user:3 name age sex

  • HGETALL:获取一个hash中 key 中的所有field和value,例如:HGETALL user:3
  • HKEYS:获取一个hash中的所有field,例如:HKEYS user:3
  • HVALS:获取一个hash中的所有value,例如:HVALS user:3
  • HINCRBY:让一个hash中的一个key进行自增,例如:HINCRBY user:3 age 2
  • HSETNX:判断field是否存在,存在不添加,不存在添加,例如:

2.5、list类型

2.5.1、结构和特征
  • 类似一中双向链表,支持正向检索和反向检索

  • 特征:

    • 有序
    • 元素可重复
    • 插入和删除快
    • 查询速度一般
  • 常用于存一些有序的数据

2.5.2、常用命令

  • LPUSH:向列表 左侧 插入一个或多个元素 (头插法)
  • LPOP:移除并返回 左侧 的第一个元素

  • RPUSH:向列表 右侧 插入一个或多个元素 (尾插法)
  • RPOP:移除并返回 右侧 的第一个元素

  • LRANGE key start end:返回一段角标范围内的所有元素
  • BLPOP,BRPOP:当没有元素时,会等待
2.5.3、思考
  • list结构模拟栈
    • 入口和出口一边
      • lpush + lpop
  • list结构模拟队列
    • 入口和出口不一边
      • lpush + rpop

2.6、set类型

2.6.1、特征
  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集、等功能
2.6.2、常见命令
  • SADD:向set中添加一个或多个元素
  • SREM:移除set中的指定元素
  • SCARD:返回set中元素的个数
  • SISMEMBER:判断一个元素是否存在于set中
  • SMEMBERS:获取set中的所有元素

  • SINTER:比较两个 key 之间的 交集
  • SDIFF:比较两个key之间的 差集
  • SUNION:比较两个key之间的 并集

2.7、SortSet

2.7.1、介绍和特征
  • Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。

  • 可排序

  • 元素不可重复

  • 查询速度快

  • 通常用于实现排行榜功能

2.7.2、常见的命令
  • ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值
  • ZREM key member:删除sorted set中的一个指定元素
  • ZSCORE key member : 获取sorted set中的指定元素的score值
  • ZRANK key member:获取sorted set 中的指定元素的排名

  • ZCARD key:获取sorted set中的元素个数
  • ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
  • ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值
  • ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
  • ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
  • ZDIFFZINTERZUNION:求差集、交集、并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可

3、redis客户端

3.1、jedis

3.1.1、小demo
  • 用idea创建一个java的maven项目

  • 引入依赖

    •     <dependency>
              <groupId>redis.clients</groupId>
              <artifactId>jedis</artifactId>
              <version>4.3.1</version>
          </dependency>
      
  • 建立链接

    • //建立链接
              JedisPool pool = new JedisPool("服务器地址", 6379);
              Jedis jedis = pool.getResource();
              //输入密码
              jedis.auth("0806");
              //选择库
              jedis.select(0);
      
  • 执行redis操作

    •     //进行操作
          jedis.set("name","lmg");
          jedis.set("age", String.valueOf(12));  //默认填字符串要转换
          System.out.println(jedis.get("name"));
          System.out.println(jedis.get("age"));
        
          Map<String,String> hash = new HashMap<>();
          hash.put("name", "John");
          hash.put("surname", "Smith");
          hash.put("company", "Redis");
          hash.put("age", "29");
          jedis.hset("book:1",hash);
          System.out.println(jedis.hgetAll("book:1"));
        
          //关闭jedis
          if (jedis !=null){
              jedis.close();
          }
      
3.1.2、建立线程安全的jedis
  • 建立一个工具类

    • public class JedisConnetionFactory {
          private static final JedisPool jedisPool;
      
          static {
              JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
              //最大链接数
              jedisPoolConfig.setMaxTotal(8);
              //最大空闲链接
              jedisPoolConfig.setMaxIdle(8);
              //最小空闲链接
              jedisPoolConfig.setMinIdle(8);
              //设置最长等待时间,5s
              jedisPoolConfig.setMaxWait(Duration.ofSeconds(5));
      
              jedisPool = new JedisPool(jedisPoolConfig,"服务器地址",6379,1000,"0806");
          }
      
          public static Jedis getJedis(){
              return jedisPool.getResource();
          }
      }
      
      
  • main函数中使用工具类创建jedis

    •     Jedis jedis = JedisConnetionFactory.getJedis();
          System.out.println(jedis.get("name"));
      jedis.close();
      

4、SpringDataRedis

4.1、初始化项目

  • 支持的功能

    • 提供了对不同Redis客户端的整合(Lettuce和Jedis)

    • 提供了RedisTemplate统一API来操作Redis

    • 支持Redis的发布订阅模型

    • 支持Redis哨兵和Redis集群

    • 支持基于Lettuce的响应式编程

    • 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化

    • 支持基于Redis的JDKCollection实现

    • image-20240118111403349

  • 1、引入依赖

    • Spring-redis依赖

      • <!--        redis依赖-->       
        	<dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-data-redis</artifactId>
                </dependency>
        <!--        pool的依赖-->
                <dependency>
                    <groupId>org.apache.commons</groupId>
                    <artifactId>commons-pool2</artifactId>
                </dependency>
        
  • 2、配置redis的信息

    • spring:
        # redis 配置
        redis:
          # 地址
          host: .服务器地址
          # 端口,默认为6379
          port: 6379
          # 密码,密码用双引号括起来,血与泪的排查(重置服务器的代价)
          password: "0806"  
          # 连接超时时
          timeout: 5200
          lettuce:
            pool:
              # 连接池中的最小空闲连接
              min-idle: 0
              # 连接池中的最大空闲连接
              max-idle: 8
              # 连接池的最大数据库连接数
              max-active: 8
              # #连接池最大阻塞等待时间(使用负值表示没有限制)
              max-wait: -1
      

4.2、修改序列化操作

  • 未修改时

    • image-20240118150234216
    • image-20240118150202955
    • 键和值都是这样显示的,而我set的确实name,显然这样是不行的。要修改序列化操作
  • @Configuration
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
            //创建RedisTemplate对象
            RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
            //创建链接工厂
            redisTemplate.setConnectionFactory(connectionFactory);
            //创建序列化工具,这个是value的
            GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
            //设置key 的序列化
            redisTemplate.setKeySerializer(RedisSerializer.string());
            //设置value的序列化
            redisTemplate.setValueSerializer(serializer);
            redisTemplate.setHashValueSerializer(serializer);
            //返回
            return redisTemplate;
        }
    }
    

4.3、StringRedisTemplate

为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。

  • json序列化器时,将user对象转json存入redis时,redis会默认存入一个user所在的地址,最后可以通过反射达到反序列化,这样会消耗redis空间
  • 我们可以用Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程:
4.3.1、自己序列化
  • 写入redis序列化将对象为json

  • 读出数据时,将json转换为对象

  • User user = new User("xxl",20);
    //对象转json
    String jsonString = JSONObject.toJSONString(user);
    stringRedisTemplate.opsForValue().set("user:11",jsonString);
    
    //json转对象
    String jsonUser = stringRedisTemplate.opsForValue().get("user:11");
    User user1 = Objects.requireNonNull(JSONObject.parseObject(jsonUser)).toJavaObject(User.class);
    
    System.out.println(user1);
    

5、redis实战

5.1、短信登录
  • 验证验证码的接口

    • image-20240119201151462
    •     /**
           * 发送手机验证码,控制层
           */
          @PostMapping("code")
          public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
              return userService.sendCode(phone,session);
          }
      
      //业务逻辑
      @Override
          public Result sendCode(String phone, HttpSession session) {
              //校验手机号,用hutool中的工具
              boolean isValid = PhoneUtil.isMobile(phone);
              //不符合返回错误
              if (!isValid){
                  return Result.fail("手机号码格式错误");
              }
              //符合生成验证码
              String str = RandomUtil.randomNumbers(6);
              //保存用户到redis,加个前缀有助于区分,时间为两分钟
              String s = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
              if (s == null) {
                  stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,str,2, TimeUnit.MINUTES);
              }
              //发送验证码
              log.debug("发送验证码成功,验证码:{}",str);
              //返回ok
              return Result.ok("ok");
          }
      
  • 登录接口的实现

    • image-20240119201326820

    •     /**
           * 登录功能
           * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
           */
          @PostMapping("/login")
          public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
              return userService.login(loginForm,session);
          }
          @GetMapping("/me")
          public Result me(){
              UserDTO userDTO = UserHolder.getUser();
              return Result.ok(userDTO);
          }
      
      
          
         //业务层 
          @Override
          public Result login(LoginFormDTO loginForm, HttpSession session) {
              String phone = loginForm.getPhone();
              String code = loginForm.getCode();
              //1、验证手机号
              if (!PhoneUtil.isMobile(phone)){
                  return Result.fail("手机号码格式错误");
              }
              //2、验证验证码,在redis中验证
              String flag = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
              if (flag == null || !flag.equals(code)) {
                  return Result.fail("验证码错误");
              }
              //3、判断用户是否存在
              User user = this.query().eq("phone", phone).one();
              //4、不存在创建一个新用户
              if (user == null){
                  user = createUser(phone);
              }
              //5、保存用户数据到redis中,用hash存,token用uuid生成,
              String token = UUID.randomUUID().toString(true);
              //6、将user对象转化为hashmap存,因为在转map的方法中默认两个类型都是String所以需要转化
              UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
              //法1、高级
              Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                      CopyOptions.create().setIgnoreNullValue(true)
                              .setFieldValueEditor((fileldName,fieldVaule) -> fieldVaule.toString()));
              //法2、笨方法
      //        HashMap<String,String> map = new HashMap<>();
      //        map.put("id",userDTO.getId().toString());
      //        map.put("nickName",userDTO.getNickName());
      //        map.put("icon",userDTO.getIcon());
      
              //7、value用来保存用户信息
              stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);//键是uuid+token
              //设置有效期,长时间未操作,清除
              stringRedisTemplate.expire(LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
              //返回token信息
              return Result.ok(token);
          }
      
          private User createUser(String phone) {
              User user = new User();
              user.setPhone(phone);
              user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
              this.save(user);
              return user;
          }
      
  • 登录校验,保证用户一直在登录状态

    • 使用拦截器,对请求进行拦截。

    • image-20240119201511681
    • @Configuration
      public class MvcConfig implements WebMvcConfigurer {
      
          @Resource
          private StringRedisTemplate stringRedisTemplate;
      
          //order为 0 的拦截所有地址,用于做token的更新和保存数据到ThreadLocal,并对redis的token有效期刷新
          //order为 1 的拦截要登录才能使用的功能,判断是否登录
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(new LoginInterceptor())
                      .excludePathPatterns(
                              "/shop/**",
                              "/voucher/**",
                              "/upload/**",
                              "/shop-type/**",
                              "/user/code",
                              "/blog/hot",
                              "/user/login"
                      ).order(1);
              registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
          }
      }
      
    • public class RefreshTokenInterceptor implements HandlerInterceptor {
      
          //无法通过注解进行构建,因为这个类是我们自己构建的,不是Spring构建的,可以通过构造器注入
          private StringRedisTemplate stringRedisTemplate;
      
          public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
              this.stringRedisTemplate = stringRedisTemplate;
          }
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //1、根据前端的请求头获取token
              String token = request.getHeader("authorization");
              if (CharSequenceUtil.isBlank(token)){
                  return true;
              }
              //2、通过token获取用户信息
              Map<Object, Object> map = stringRedisTemplate.opsForHash()
                      .entries(LOGIN_USER_KEY + token);
              //3、判断用户是否存在
              if (map.isEmpty()){
                  return true;
              }
              //4、将map转换为UserDTO,hutool里面的工具
              UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
              //5、存在保存信息到ThreadLocal
              UserHolder.saveUser(userDTO);
              //6、刷新token的有效期,达到用户一直访问,一直有效
              stringRedisTemplate.expire(LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
              //放行
              return true;
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              UserHolder.removeUser();
          }
      }
      
    • public class LoginInterceptor implements HandlerInterceptor {
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //1、判断是否要拦截
              if (UserHolder.getUser() == null){
                  response.setStatus(401);
                  return false;
              }
              return true;
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              UserHolder.removeUser();
          }
      }
      
文章来源:https://blog.csdn.net/weixin_51713937/article/details/135706464
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。