Redis(1)-基本概念及使用

Redis是一个高性能的内存数据库,以其单线程、数据持久化、丰富的数据类型等特点被广泛用于缓存、消息中间件等场景。本文详细介绍了Redis的性能优势、数据类型以及如何在Spring中使用Redis作为缓存,包括RedisTemplate和SpringCache的集成应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 一、Redis是什么

  • Redis是一个由C 语言开发的一个高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。
  • 它是一种NoSQL(not-only sql,泛指非关系型数据库)的数据库。


        特点:
            1.性能优越,数据存储在内存中,读写速度非常快,支持10W QPS。
            2.单线程单进程,线程是安全的,同时避免了线程上下文的切换带来的损耗。
            3.采用多路IO多路复用机制。
            4.支持丰富的数据类型:字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
            5.支持数据持久化,可以将内存中数据保存在磁盘中,有RDB和AOF方式。
            6.支持主从复制、哨兵模式。
            7.另外也可以用作分布式锁、可以作为消息中间件使用、支持发布订阅。
            

 二、Redis为什么这么快

1. 因为Redis完全是基于内存操作,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,类似于HashMap(HashMap的优势就是查找和操作的时间复杂度是O(1))。
2. 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的CPU,也不用考虑锁所带来的问题。

(官方解释:因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。)
3. 使用多路复用IO模型,非阻塞IO。

备注:

  • 1.为什么Redis在6.0之后加入了多线程(在某些情况下,单线程出现了缺点,多线程可以解决?)

            引入多线程说明Redis在有些方面,单线程已经不具有优势了。
            因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。
            Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想Redis因为多线程而变得复杂,需要去控制key、lua、事务、LPUSH/LPOP等等的并发问题。
            举例:
                我们知道Redis可以使用del命令删除一个元素,如果这个元素非常大,可能占据几十兆或者是几百兆,那么在短时间内是不能完成的,这样一来就需要多线程的异步支持,现在删除工作可以在后台进行。

三、Redis支持的五种数据类型

首先,Redis内部使用一个redisObject对象来表示所以的key和value。

redisObject最主要的信息有:

        1.type:表示一个value对象具体是何种数据类型,一般有5种,在后续讲解。
        2.encoding是不同数据类型在Redis内存的存储方式。
        
        比如:type = string表示value存储的是一个普通字符串,那么encoding可以是raw或者int。

    

  • Redis中5种基本数据类型:
类型简介特性场景
string (字符串)二进制安全可以包含任何数据,  比如jpg图片或者序列化对象...
hash (字典)键值对集合, 即编程语言中的map类型适合存储对象,并且可以像数据库一样update一个属性存储、读取、修改用户属性
list (列表)链表(双向链表)增删快,提供了操作某一元素的api最新消息排行;消息队列
set (集合)hash表实现,元素不重复添加、删除、查找的复杂度都是O(1), 提供了求交集、并集、差集的操作共同好友;利用唯一性,统计访问网站的所有ip
sorted set (有序集合)set集合中增加score参数,元素按score有序排序数据插入集合时,已经进行了天然排序排行榜;带权重的消息队列

扩展类型:

  • HyperLogLog:将数据哈希后分桶统计,结果存储在 String 中。
  • Bitmaps:通过 SETBITGETBIT 等命令操作 String 的二进制位。
  • GEO:使用 ZSet 存储经纬度的 GeoHash 编码,通过 GEOADD 等命令封装地理计算。

扩展数据结构

底层实现

用途

HyperLogLog

封装后的 String

近似基数统计(如UV去重)

Bitmaps

String 的位操作

布尔标记、位统计

GEO

ZSet + GeoHash

地理位置存储与查询

Stream

链表 + 消息队列

消息流(类似Kafka)

Redis基础类型 vs 扩展类型:

类别

特点

示例

基础类型

直接对应存储结构,支持通用操作

String、List、Hash、Set、ZSet

扩展类型

基于基础类型封装,解决特定场景问题

HyperLogLog、Bitmaps、GEO


四、Redis缓存使用

  • 直接通过 RedisTemplate 来使用,使用 Spring Cache 集成 Redis

(1)pom.xml 中加入以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>


(2)配置文件 application.yml 的配置:

server: port: 8082 
servlet: session: timeout: 30ms
spring:cache:type: redis 
redis: host: 127.0.0.1 
       port: 6379
       password:# redis默认情况下有16个分片,这里配置具体使用的分片,默认为0
       database: 0 
       lettuce: pool:# 连接池最大连接数(使用负数表示没有限制),默认8
       max-active: 100


(3)创建实体类 User.java:

public class User implements Serializable{
    private static final long serialVersionUID = 662692455422902539L;
    private Integer id;
    private String name;
    private Integer age;
    public User(){ }
    public User(Integer id, String name, Integer age){
        this.id = id;
        this.name = name;
        this.age = age; 
    }
    public Integer getId(){
        return id; 
    }
    public void setId(Integer id){
        this.id = id; 
    }
    public String getName(){
        return name; 
    }
    public void setName(String name){
        this.name = name; 
    }public Integer getAge(){
        return age; 
    }
    public void setAge(Integer age){
        this.age = age; 
    }
    @Override
    public String toString(){
        return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +'}'; 
    }
}


(4.1)RedisTemplate 的使用方式

默认情况下的模板只能支持 RedisTemplate<String, String>,也就是只能存入字符串,所以自定义模板很有必要。

添加配置类 RedisCacheConfig.java:

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheConfig { 
    @Bean
    public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory connectionFactory) { 
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(connectionFactory);returntemplate; 
    }
}

测试类:

@RestController
@RequestMapping("/user")
public class UserController{
    public static Logger logger = LogManager.getLogger(UserController.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisTemplate<String, Serializable> redisCacheTemplate;
    @RequestMapping("/test")
    public void test() {
        redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25)); 
        User user = (User) redisCacheTemplate.opsForValue().get("userkey");
        logger.info("当前获取对象:{}", user.toString()); 
    }
}

(4.2)使用 Spring Cache 集成 Redis

Spring Cache 具备很好的灵活性,
不仅能够使用 SPEL(spring expression language)来定义缓存的 Key 和各种 Condition,还提供了开箱即用的缓存临时存储方案,
也支持和主流的专业缓存如 EhCache、Redis、Guava 的集成。

定义接口 UserService.java:

public interface UserService {
    User save(User user);
    void delete(int id);
    User get(Integer id);
}

接口实现类 UserServiceImpl.java:

@Service
public class UserServiceImpl implements UserService{
    public static Logger logger = LogManager.getLogger(UserServiceImpl.class);
    private static Map<Integer, User> userMap = new HashMap<>();
    static { 
        userMap.put(1, new User(1, "肖战", 25)); 
        userMap.put(2, new User(2, "王一博", 26)); 
        userMap.put(3, new User(3, "杨紫", 24)); 
    }
    
    @CachePut(value ="user", key = "#user.id")
    @Override
    public User save(User user){ 
        userMap.put(user.getId(), user); 
        logger.info("进入save方法,当前存储对象:{}", user.toString());
        return user; 
    }
    
    @CacheEvict(value="user", key = "#id")
    @Override public void delete(int id){ 
        userMap.remove(id); 
        logger.info("进入delete方法,删除成功"); 
    }
    
    @Cacheable(value = "user", key = "#id")
    @Override public User get(Integer id){ 
        logger.info("进入get方法,当前获取对象:{}", userMap.get(id)==null?null:userMap.get(id).toString());
        return userMap.get(id); 
    }
}

为了方便演示数据库的操作,这里直接定义了一个 Map<Integer,User> userMap。

这里的核心是三个注解:

@Cachable@CachePut@CacheEvict

测试类:UserController

@RestController
@RequestMapping("/user")
public class UserController{
    public static Logger logger = LogManager.getLogger(UserController.class);
    @Autowired 
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisTemplate<String, Serializable> redisCacheTemplate;
    @Autowired
    private UserService userService;
    @RequestMapping("/test")
    public void test(){ 
        redisCacheTemplate.opsForValue().set("userkey", new User(1, "张三", 25)); 
        User user = (User) redisCacheTemplate.opsForValue().get("userkey");
        logger.info("当前获取对象:{}", user.toString());
    }
    @RequestMapping("/add")
    public void add(){ 
        User user = userService.save(new User(4, "李现", 30)); 
        logger.info("添加的用户信息:{}",user.toString());
    }
    @RequestMapping("/delete")
    public void delete(){ 
        userService.delete(4);
    }
    @RequestMapping("/get/{id}")
    public void get(@PathVariable("id") String idStr) throws Exception{
        if (StringUtils.isBlank(idStr)) {
            thrownew Exception("id为空"); 
        } 
        Integer id = Integer.parseInt(idStr); 
        User user = userService.get(id); 
        logger.info("获取的用户信息:{}",user.toString()); 
    }
}

用缓存要注意,启动类要加上一个注解开启缓存:

@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
@EnableCaching
public class Application{
    public static void main(String[] args){ 
        SpringApplication.run(Application.class, args); 
    }
}

  

备注:

1)为什么使用redis做缓存而不是其他的消息队列如kafka?

核心功能与设计目标不同

维度

Redis

Kafka

核心用途

高速缓存、键值存储、实时数据访问

高吞吐量消息队列、流数据处理、日志聚合

数据时效性

毫秒级延迟,适合实时读写

高吞吐但延迟较高(通常毫秒到秒级)

数据存储

内存为主,支持持久化

磁盘持久化,长期存储海量数据

数据结构

支持丰富数据结构(字符串、哈希、列表等)

仅支持字节流,需应用层解析

消费模式

直接通过键访问

基于消费者组的顺序或并行拉取

2)设计redis的key和value上,有哪些原则

1)Key 的设计原则

  • 可读性:Key 应易于理解,清晰表达其所关联的值的意义。例如,使用 user:basic.info:userid:string 这样的命名方式,通过在名称中包含值类型来增强可读性。
  • 简洁性:保持 Key 简短但有意义,以节省内存并提高性能。避免过于简略导致与其他 Key 冲突。
  • 字符限制:避免使用特殊字符,确保 Key 易于阅读和操作。建议仅使用大小写字母、数字及特定符号(如竖线、下划线、英文点号“.”和英文冒号“:”)。
  • 命名空间:利用前缀创建命名空间,区分不同类型的数据。比如,用户数据可用 user: 开头,缓存数据则用 cache: 前缀。
  • 长度控制:避免过长的 Key 名称,以免占用过多内存。

2)Value 的设计原则

  • 数据类型选择:根据数据特点选择最合适的 Redis 数据类型(如字符串、列表、哈希、集合或有序集合),以优化操作效率。
  • 避免大Value:尽量避免存储过大的 Value,因为这可能导致性能问题。考虑将大数据分割为多个小 Value 以提高处理速度和降低内存消耗。
  • 设置过期时间:为 Value 定义适当的过期时间,以便自动清理不再需要的数据,减少内存占用。
  • 压缩数据:对于可压缩的数据,在存储之前进行压缩可以有效减少内存使用。
  • 数据结构内存编码优化:合理配置数据结构的内存编码。例如,ziplist 对于小型列表、哈希表和有序集合特别有效,因为它允许这些结构共享连续的内存块。然而,由于 ziplist 缺乏索引支持,频繁的查找、插入或删除操作可能会导致性能下降。因此,应基于实际需求评估是否采用 ziplist 或其他更合适的数据结构。

3)Redis中多大的key算热key,该如何解决,怎么避免热点问题

3-1)热key的识别

  • 单个key的QPS远高于其他Key(如超过1000QPS)
  • 某个Key的请求量占集群总请求量的5%~10%以上

3-2)如何识别热key

  • redis-cli --hotkeys
    使用 Redis 自带的命令(需开启 LFU 算法,maxmemory-policy 配置为 allkeys-lfuvolatile-lfu)。
  • 第三方监控工具
    如 Prometheus + Grafana、Redis 的 INFO 命令统计、商业监控工具(如阿里云 DAS)。

3)解决方案

3-1)本地缓存(Local Cache)

        原理:将热 Key 数据缓存在应用服务器的本地内存(如 Guava Cache、Caffeine),减少直接访问 Redis 的次数。

注意事项:

        设置合理的过期时间,避免数据不一致。

3-2)数据分片(Sharding)

        原理:将热 Key 拆分为多个子 Key,分散到不同节点。例如:

hot_key → hot_key:1、hot_key:2、hot_key:3

        实现方式:客户端一致性哈希:根据用户 ID 或随机数选择子 Key。

3-3)限流与降级(Rate Limiting & Degradation)

        原理:对热 Key 的请求限流,或在极端情况下返回降级内容(如默认值)。

        工具:使用 Sentinel、Hystrix 等框架实现熔断机制。

3-4)读写分离(Read/Write Splitting)

        原理:通过 Redis 主从架构,将读请求分流到从节点。

        优化:使用 Redis ClusterSentinel 自动管理读写分离。

4)Redis的zset的原理

底层分别使用ziplist(压缩链表)skiplist(跳表)实现。

4-1)当zset满足以下两个条件的时候,使用ziplist:

1. 保存的元素少于128个
2. 保存的所有元素大小都小于64字节

4-2)不满足这两个条件则使用skiplist。

压缩链表:就是将元素值按照分值从小到大排列

跳表:除了常规的双向链表,还增加了一个分层数据结构,随机设置跳转层数,主打随意跳转至目前前后。

5)如何用Redis统计亿级网站的UV?

使用HyperLogLog

        HyperLogLog 是一种概率数据结构,用于估计集合的基数。每个 HyperLogLog 最多只需要花费 12KB 内存,在标准误差 0.81%的前提下,就可以计算 2 的 64 次方个元素的基数。

HyperLogLog 的优点在于它所需的内存并不会因为集合的大小而改变,无论集合包含的元素有多少个,HyperLogLog 进行计算所需的内存总是固定的,并且是非常少的。

主要特点如下。

  • 高效的内存使用:HyperLogLog 的内存消耗是固定的,与集合中的元素数量无关。这使得它特别适用于处理大规模数据集,因为它不需要存储每个不同的元素,只需要存储估计基数所需的信息。
  • 概率估计:HyperLogLog 提供的结果是概率性的,而不是精确的基数计数。它通过哈希函数将输入元素映射到位图中的某些位置,并基于位图的统计信息来估计基数。由于这是一种概率性方法,因此可能存在一定的误差,但通常在实际应用中,这个误差是可接受的。
  • 高速计算:HyperLogLog 可以在常量时间内计算估计的基数,无论集合的大小如何。这意味着它的性能非常好,不会受到集合大小的影响。

6)zset底层原理

Redis 的 zset 由两种数据结构组合实现:

    1)如果有序集合的成员个数小于 128 个,并且每个成员的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
    2)如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

6-1)压缩列表(ziplist)(在Redis7.0中,压缩列表数据结构已经废弃,交由listpack数据结构来实现了)
        结构:ziplist 是一种紧凑的连续内存块结构,在存储 ZSet 时,元素按照分数从小到大的顺序排列,成员和分数依次交替存储在内存中,每个节点包含前一个节点的长度、当前节点的长度以及具体的数据内容。


        优点:在元素数量较少且元素大小较小时,内存使用效率极高,因为它避免了指针带来的额外内存开销,并且是连续存储,缓存命中率高。
        缺点:插入和删除操作可能会导致大量的数据移动,时间复杂度为 O (n);查找元素也需要遍历整个列表,效率较低;同时,存在连锁更新问题,即一个节点的长度变化可能引发后续多个节点的更新,影响性能。

6-2)Listpack
        结构:Listpack 同样是紧凑的连续内存结构,它改进了 ziplist 的节点布局,去除了记录前一个节点长度的字段,每个节点只包含自身长度和数据内容,使得结构更加紧凑。


        优点:解决了 ziplist 的连锁更新问题,在插入和删除操作上性能有所提升;并且内存使用效率进一步提高,能够更有效地存储数据。
        缺点:和 ziplist 类似,当元素数量增多时,查找操作仍需要遍历列表,时间复杂度为 O (n),对于大规模数据的操作性能不如 skiplist。


6-3)跳表(skiplist)+ 哈希表(hashtable)
        结构:跳表是一种基于链表的有序数据结构,每个节点包含元素的成员、分数以及多层指针,通过这些多层指针可以快速跳过一些节点,从而提高查找效率;同时,搭配一个哈希表,用于存储元素成员和分数的映射关系。


        优点:插入、删除和查找操作的平均时间复杂度为 O (log n),能够高效地处理大量数据;支持范围查询,可以快速获取指定分数范围内的元素;并且实现相对简单,易于维护。
        缺点:相比于 ziplist 和 Listpack,内存占用较大,因为每个节点需要额外的指针空间;哈希表也需要一定的内存开销;而且在元素数量较少时,其性能优势不明显,会有额外的内存和性能损耗。

跳表:

哈希表:

注意:在Redis7.0中,压缩列表数据结构已经废弃,交由listpack数据结构来实现了。

Redis讲解目录:

Redis(1)-基本概念及使用

Redis(2)-Redis的一些问题及策略

Redis(3)-集群:主从复制、哨兵模式、Cluster

Redis(4)-Redis遇到的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sun cat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值