【主流技术】聊一聊 Redis 的基本结构和简单应用(一)

发布时间 2023-11-10 10:32:32作者: Apluemxa

前言

Redis 是目前互联网后端的热门中间件之一,在许多方面都有深度的应用,作为后端开发熟练掌握该技术是十分有必要的。

Redis 的五种数据类型是:1、String(字符串);2、Hash(哈希);3、List(列表);4、Set(集合);5、Sort Set (有序集合)。其余的用的比较少,本文暂不涉及。

其中,String(字符串)是 Redis 中最基本的数据类型,一个 Key 对应一个 Value。其它的几个常用结构如下简图所示:

图1

关于 Redis 的安装搭建和在 Linux 中的原生命令操作,可以见我的另一篇文章:https://www.cnblogs.com/Apluemxa/p/16465276.html

注:以下内容都是基于 RedisTemplate 在项目中的实际使用进行说明。

  1. 引入 pom 依赖

    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->       
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 新建序列化配置类

    @Configuration
    public class RedisTemplateConfiguration {
        //需要单独声明该 Bean 的 name,否则使用 JDK 自带的序列化配置会导致显示乱码
        @Bean(name = "redisTemplate")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String , Object> template = new RedisTemplate<>();
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            template.setConnectionFactory(redisConnectionFactory);
            //key 序列化
            template.setKeySerializer(redisSerializer);
            //value 序列化
            template.setValueSerializer(redisSerializer);
            //value 的 hash 序列化
            template.setHashValueSerializer(redisSerializer);
            //key 的 hash 序列化
            template.setHashKeySerializer(redisSerializer);
            return template;
        }
    }
    
  3. 新建 RedisTemplate 工具类

    作用是封装一些常用的方法(只给两个简单的方法示例,直接注入 RedisTemplate 效果也一样),同时在方法里进行一些判空、异常捕获、输出日志等操作。

    @Slf4j
    @Component
    public class RedisTemplateUtils {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
        
        /**
         * 放入普通缓存,并设置过期时间
         * @param key 键
         * @param value 值
         * @param expireTime  过期时间(秒) ,time 要大于0,如果小于等于0,将设置无限期
         * @return true 成功,false 失败
         */
        public Boolean set(String key, Object value, long expireTime) {
            try {
                if (expireTime > 0) {
                    redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return Boolean.TRUE;
            } catch (Exception e) {
                log.error("exception when set Redis key {}. ", key, e);
                return Boolean.FALSE;
            }
        }
        
        /**
         * 获取普通缓存
         * @param key 键
         * @return Object 类型的值
         */
        public Object get(String key) {
            return Optional.ofNullable(redisTemplate.opsForValue().get(key)).orElse(null);
        } 
    }
    

一、String 类型

字符串类型是 Redis 中最基本的数据存储类型,它是一个由字节组成的序列,在 Rediss 中是二进制安全的。这意味着该类型可以接受任何格式数据,如 JPEG 图像链接数据和 Json 对象格式的信息等等。

它是标准的 key-value,通常用于存储字符串、整数和浮点。其中单个 value 内可容纳最高达512MB的数据。

‎由于所有数据都在单个对象中,Redis 中的字符串操作速度非常快。‎‎基本的‎‎ Redis 命令(如 ‎‎SET‎‎、‎‎GET‎‎ 和 ‎‎DEL‎‎)允许对字符串值执行一些基本操作:

  • ‎SET 键值‎‎ ‎‎– 设置指定键的值。‎
  • ‎GET 键‎‎ ‎‎– 检索指定键的值。‎
  • ‎DEL 键‎‎ ‎‎– 删除给定键的值。‎
    @Resource
    private RedisTemplateUtils redisTemplateUtils;
    /**
     * String 类型的存/取
     * @return
     */
    @Override
    public String testStringType() {
        String redisStr = "";
        //设置键、值以及过期时间30分钟
        if (redisTemplateUtils.set(SysConstant.TEST_REDIS_KEY, UUIDUtils.generateUUID(), 1800)){
            //由于获得的值是 Object 类型的,所以需要强转成 String 类型
            redisStr = (String) redisTemplateUtils.get(SysConstant.TEST_REDIS_KEY);
            System.out.println(redisStr);
        }
        return redisStr;
    }

注:其中需要先注入 RedisTemplateUtils 来保证引入 RedisTemplate。

Redis 的可视化客户端(我用的是Another Redis Desktop Manager,开源免费)中的数据可以看到已经写入了,如下图1-1所示:

图1-1

常见的应用场景:存储用户 token 信息、缓存热点关键数据、统计站点访问量、计算当前在线人数等。


二、List 类型

Redis 列表是简单的字符串列表,按照插入顺序排序。我们可以添加一个元素到列表的头部(左边)或者尾部(右边)。

Redis 的列表允许用户从列表的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是一个链表的结构,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素速度就越快。

这意味着,即使有数以千万计的元素列表,也可以极快地速度获得10条在头部或者尾部的记录。

该种类型的‎‎字符串链表‎‎可以执行一些常见的基本操作,例如:‎

  • ‎leftPushAll‎ – 将值从左边推送到列表(倒序)。‎
  • ‎rightPushAll‎ – 将值从右边推送到列表(顺序)。‎
  • ‎RANGE‎‎ – 根据 key 获取 value。‎
  • ‎LPOP/RPOP‎‎ ‎‎– 用于显示和删除列表两端的值。‎
  • ‎LINDEX‎‎ ‎‎– 获取列表中指定位置(下标)的值。
    @Resource
    private StudyMapper studyMapper;
    
    //为了方便举例,这里直接用 RedisTemplate 实现,就不用自己封装的工具类了,效果是一样的
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 测试 List 类型
     * @return
     */
    @Override
    public List<String> testListType() {
        LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>();
        //先将数据库的实体类型列表转换为 String 类型
        List<String> stringList = studyMapper.selectList(wrapper).stream()
            .map(val -> val.convertExt(String.class))
            .collect(Collectors.toList());
        //数据先存后取
        if (redisTemplate.opsForList().leftPushAll(SysConstant.TEST_REDIS_LIST_KEY, stringList) > NumberUtils.LONG_ZERO){
            //参数为0和-1,代表取出所有值
            List<String> list = redisTemplate.opsForList().range(SysConstant.TEST_REDIS_LIST_KEY,0,-1);
            return list;
        }
        return null;
    }

在 Redis 可视化客户端的数据如下图2-1所示:

图2-1

常见的应用场景:xx最新排行榜前十;消息队列(订阅/发布模式)等。


三、Hash 类型

Redis hash 是一个键值对(其中 key 数大于等于 value 数)的集合。Redis hash 是一个 field 和 value 都为 String 类型的映射表,hash 特别适合用于存储集合对象。

Redis的Hash结构可以使你像在数据库中 update 一个属性那样,只修改某一项属性值。和 String 有点像,但 value 中存放的是一张表,一般用于多个个体的详细事项排列,String 也可以做到,但要比 hash 麻烦许多。

以下是该结构相关的一些方法,允许更改单个或多个字段:

  • ‎HSET‎‎ – 根据键向哈希表放入数据。‎
  • ‎HGET‎‎ –根据 key 获取各个值。‎
  • ‎HGETALL‎‎ ‎‎– 获取整个哈希表的内容。‎
  • ‎HDEL‎‎ – 从哈希中删除现有的键值对。‎
    @Resource
    private ShoppingCarMapper shoppingCarMapper;    

    /**
     * 测试 Hash 类型
     * @return
     */
    @Override
    public Map<Object, Object> testHashType() {
        LambdaQueryWrapper<ShoppingCar> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCar::getUserId,"1656698374114156635");
        //组装一个 key 为 商品Id、value 为该用户购物车信息 list 的 Map 对象
        Map<String, String> hashMap = new HashMap<>();
        shoppingCarMapper.selectList(wrapper).forEach(val -> hashMap.put(val.getGoodId(), val.convertExt(String.class)));
        //redis 的 key 为用户 userId
        if(redisTemplateUtils.hset("1656698374114156635",hashMap,1800)){
            return redisTemplateUtils.hget("1656698374114156635");
        }
        return null;
    }

在 Redis 可视化客户端的数据如下图3-1所示:

图3-1

常见的应用场景:频繁更改的数据,如用户的购物车、用户会话信息等;适合使用 Hash 结构存储的数据:如城市与所处经纬度、城市与所处的学校等。


四、Set 结构

Redis 的 Set 是 String 类型的无序不重复集合。Set的底层是借助哈希表来实现的,所以添加、删除、查找的复杂度都是 O(1)。

所谓 Set 集合就是一堆不重复值的组合,并且这些值的摆放是没有顺序的。

如:在微博应用中,可以将某个用户所有的关注人存入一个集合中,将其所有的粉丝存入另一个集合。

Redis还提供了诸如collection、union和differences等操作,使得实现诸如commandism、poperhike、secondfriends这样的功能变得很容易,或者可以选择将结果返回,还是将它们保存到新的集合中。

可以使用‎‎以下命令‎‎添加、删除、检索和检查等,对集合中的内容进行操作:‎

  • ‎SADD‎‎ – 向集合中添加一个或多个元素。‎
  • SISMEMBER‎ – 判断set集合中是否包含指定值。‎
  • ‎SMEMBERS‎‎ – 根据 key 获取集合中所有元素。‎
  • ‎SREM‎‎ – 从集合中删除现有元素。
    @Resource
    private UserMapper userMapper;
    
    /**
     * 测试 Set 类型
     * @return
     */
    @Override
    public Set<Object> testSetType() {
        //这里从数据库中构建出一个 String[]
        String[] strs = userMapper.selectList(new LambdaQueryWrapper<User>()
                        .select(User::getUnionId)).stream()
                        .map(val -> val.convertExt(String.class)).toArray(String[]::new);
        //可以放单个的 String 字符串,也可以一次性放一个 String[]
        if (redisTemplateUtils.sSet(SysConstant.TEST_REDIS_SET_KEY, strs) > NumberUtils.LONG_ZERO){
            Set<Object> result = redisTemplateUtils.sGet(SysConstant.TEST_REDIS_SET_KEY);
            return result;
        }
        return null;
    }

在 Redis 可视化客户端的数据如下图4-1所示:

图4-1

常见的应用场景:判断用户是否在线(结合过期时间)、记录文章或者商品的标签(结合去重)、交集/并集寻找共同好友等。


五、Sort Set (Zset)结构

Sorted Set 也叫 Zset ,和 Set 一样也是 String 类型元素的集合,且不允许重复的成员。不同的是每个元素都会携带一个 Double 类型的双精度浮点数。

Zset 正是通过上述的分数来为集合中的成员进行从小到大的排序。Zset的 value 是唯一的,但分数(权重)却可以重复。

以下的一些基本命令可以根据 value 或分数大小进行获取、添加、删除、检索等的操作:‎

  • ‎ZADD‎‎ ‎‎– 将具有分数的元素添加到集合。‎
  • ‎ZRANGE‎‎ ‎‎– 获取经过的排序的集合。‎‎withscores‎‎ ‎‎选项生成实际分数值(从小到大排列)。‎
  • ‎ZRANGEBYSCORE ‎‎– 按照定义的分数范围从集合中获取元素。‎‎withscores‎‎ ‎‎选项生成实际分数值。‎
  • ‎ZREM‎‎ –‎‎从已排序的集中删除元素。
    @Resource
    private ShoppingCarMapper shoppingCarMapper;
    /**
     * 测试 ZSet 类型
     * @return
     */
    @Override
    public Set<String> testZSetType() {
        //这里先初始化一个 Redis 中对 ZSet 专门设置的类型对象,其中一个元素是 value,另一个是 Double 类型的 Score 分数
        Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
        shoppingCarMapper.selectList(new LambdaQueryWrapper<ShoppingCar>()
                        .eq(ShoppingCar::getUserId, "1656698374114156635"))
                .forEach(val -> typedTupleSet.add(new DefaultTypedTuple<>(val.convertExt(String.class), val.getPrice())));
        if (redisTemplate.opsForZSet().add("1656698374114156635", typedTupleSet) > NumberUtils.LONG_ZERO){
            //这里是按照分数从小到大的顺序获取集合,reverseRange() 则顺序相反
            Set<String> result = redisTemplate.opsForZSet().range("1656698374114156635", 0, -1);
            return result;
        }
        return null;
    }

在 Redis 可视化客户端的数据如下图5-1所示:

图5-1

常见的应用场景:根据游戏段位(作为Score)生成排行榜、按照发布时间范围(作为Score)来展示文章、按照商品价格(作为Score)去排序等。


六、文章小结

本文介绍了关于 Redis 的几个基本数据结构,以及在 Spring 项目中 RedisTemplate 的一些简单使用。

如有错误,还望指正,同时也欢迎大家在评论区说出自己的想法。

最后,我有计划写一篇关于上述几种数据结构在真实场景中具体应用文章,尝试着给出一些解决方案,还请期待。

参考文档: