开箱即用,一键集成 Redis 缓存

Spring Boot 作为主流微服务框架,拥有成熟的社区生态。市场应用广泛,为了方便大家,整理了一个基于spring boot的常用中间件快速集成入门系列手册,涉及RPC、缓存、消息队列、分库分表、注册中心、分布式配置等常用开源组件,大概有几十篇文章,陆续会开放出来,感兴趣同学可以关注&收藏

简介

Redis 是一个开源、高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。支持更丰富的数据结构,例如 String、List、hash、 set、 zset 等,同时支持数据持久化。

除此之外,Redis 还提供一些类数据库的特性,比如事务,HA,主从备份。可以说 Redis 兼具了缓存系统和数据库的一些特性。

Redis特性

  • 高并发读写
  • 持久化
  • 丰富的数据类型
  • 单进程单线程模型
  • 数据自动过期
  • 发布订阅
  • 分布式
  • 支持lua脚本
  • 目前在从阿里巴巴、美团、百度、拼多多、快手等一线大厂到五六线小厂中广泛使用,对系统的高并发能力贡献极大,深受好评,开源社区非常活跃。

    数据类型

    1、String

    二进制的字符串,最简单的k-v存储,类似于memcached的存储结构,它不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度512M。支持丰富的操作命令,如:

  • GET/MGET
  • SET/SETEX/MSET/MSETNX
  • INCR/DECR
  • GETSET
  • DEL
  • 2、Hash

    采用主子key存储信息,由field和关联的value组成Map。比如计数器,key表示帖子id,field表示点赞数、评论数、转发数等,value则表示计数值。常用命令:

  • HGET/HMGET/HGETALL
  • HSET/HMSET/HSETNX
  • HEXISTS/HLEN
  • HKEYS/HDEL
  • HVALS
  • 3、List

    该类型是一个有序的元素集合,基于双向链表实现。比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。常用命令:

  • LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
  • LINDEX/LRANGE
  • LLEN/LTRIM
  • 4、Set

    Set类型是一种无顺序集合。它和List类型最大的区别是:集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。底层是通过哈希表实现的。常用命令:

  • SADD/SPOP/SMOVE/SCARD
  • SINTER/SDIFF/SDIFFSTORE/SUNION
  • 5、Sorted Set

    是set的增强版本,有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。常用命令:

  • ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT
  • ZINTER/ZDIFF/ZDIFFSTORE/ZUNION
  • 适用场景

    1、高性能缓存。缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以Redis用在缓存的场合非常多。

    作为缓存使用时,一般有两种方式保存数据:

  • 读取前,先去读Redis。如果没有数据,读取数据库,然后将数据预热到Redis。
  • 写入时,先更新数据库,然后再写入Redis。
  • 2、丰富的数据类型,满足多样化业务需求。

    3、分布式锁

    在很多互联网公司中都使用了分布式技术,分布式技术带来的挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的set nx功能来编写分布式锁,如果设置返回1说明获取锁成功,否则获取锁失败。

    4、消息队列

    Redis 中 list 的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过 lpush 将消息放入 list,消费者便可以通过 rpop 取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择 sorted set。而 pub/sub 功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。

    5、其他场景,如:秒杀、限流、计数器、排行榜、实时系统、共享session等

    单线程的Redis为什么快

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 合理高效的数据结构
  • 采用了非阻塞I/O多路复用机制(有一个文件描述符同时监听多个文件描述符是否有数据到来)
  • 如何实现键值对的快速访问

    Redis 使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。每个哈希桶中保存了键值对数据。

    当然哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

    哈希表的最大优势就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。我们只需要计算key的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。

    hash值并不是唯一的,当面对海量数据存储,计算时可能会存在哈希冲突,导致两个entry落在同一个哈希桶中。解决方式也比较简单,引入链式哈希。同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

    项目实战

    在pom.xml 中引入Spring Boot 官方提供的 starter组件

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

    spring-boot-starter-data-redis 依赖于 spring-data-redislettuce

    Spring Boot 1.X 版本默认使用的是 Jedis 客户端。2.X 版本替换成 Lettuce  客户端,如果习惯使用 Jedis 的话,可以从 spring-boot-starter-data-redis 中排除 Lettuce 并引入 Jedis。

    Lettuce 是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀 netty NIO 框架来高效地管理多个连接。

    application.yaml 配置redis的地址信息以及lettuce 连接池参数

    spring:
      redis:
        host: 127.0.0.1
        port: 6379
        #    password: abEvH46*YsH&S25d89
        lettuce:
          pool:
            maxIdle: 1000  # 连接池中的最大空闲连接
            minIdle: 2  # 连接池中的最小空闲连接
            maxWait: 10  # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
            maxActive: 1000 # 连接池最大连接数(使用负值表示没有限制)

    初始化 redis的 RedisTemplate 模板bean实例

    @Configuration
    public class RedisConfig {

        @Bean
        RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
            final StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(factory);
            template.setKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(new GenericToStringSerializer<>(Object.class));
            template.setValueSerializer(new GenericToStringSerializer<>(Object.class));
            template.afterPropertiesSet();
            return template;
        }
    }

    RedisTemplate 提供了多种类型的数据类型操作接口,满足多场景的业务需求。

    接下来就可以通过单元测试来验证缓存效果了

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = StartApplication.class)
    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public class UserMapperTest {

        @Resource
        private CacheService cacheService;

        @Test
        public void test1_set1() {
            boolean result = cacheService.set("k1", "微观技术");
            System.out.println(result);
        }

        @Test
        public void test2_get1() {
            String cacheResult = cacheService.get("k1");
            System.out.println("k1 的缓存结果:" + cacheResult);
        }
    }

    上面讲的都是通过手动方式写入、删除、查询缓存,缓存的处理逻辑散落在业务代码中。有没有更简单的方式?比如调用一个方法,通过方法上标注的注解自动从缓存中获取,如果查找不到再从数据库查,并自动将结果预热到缓存中。

    Spring 注解式缓存

    首先通过 RedisCacheConfiguration 生成默认配置,然后对缓存进行自定义化配置,比如过期时间、缓存前缀、key/value 序列化方法等,然后构建出一个RedisCacheManager,其中通过keySerializationPair 方法为 key 配置序列化,valueSerializationPair方法为 value 配置序列化。

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .prefixKeysWith("cache:user:")
                .disableCachingNullValues()
                .serializeKeysWith(keySerializationPair())
                .serializeValuesWith(valueSerializationPair());

        Map map = new HashMap<String, RedisCacheConfiguration>();
        map.put("user", redisCacheConfiguration);

        return RedisCacheManager.builder(factory)
                .withInitialCacheConfigurations(map).build();
    }

    private RedisSerializationContext.SerializationPair<String> keySerializationPair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
    }

    private RedisSerializationContext.SerializationPair<Object> valueSerializationPair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
    }

    修改、删除、查询等常见操作,官方都提供了对应的注解类,只需要在对应的方法上标注即可享受缓存功能,对研发同学及其便利,可以将精力专注到其它业务逻辑处理上。

    @Component
    @CacheConfig(cacheNames = "user")
    public class UserService {

        @Cacheable(key = "#id")
        public User getUserById(Long id) {
            User user = User.builder().id(id).userName("雪糕( " + id + ")").age(18).address("杭州").build();
            return user;
        }

        @CachePut(key = "#user.id")
        public User updateUser(User user) {
            user.setUserName("雪糕(新名称)");
            return user;
        }

        @CacheEvict(key = "#id")
        public void deleteById(Long id) {
            System.out.println("db删除数据:" + id);
        }
    }

    常用注解类

    1、 @CacheConfig 类级别的缓存注解,允许共享缓存名称

    2、 @Cacheable 一般用于查询操作,根据 key 查询缓存

  • 如果 key 存在,直接返回缓存中的数据。
  • 如果 key 不存在,查询 db,并将结果更新到缓存中。
  • 3、 @CachePut 一般用于更新和插入操作,每次都会请求 db,然后通过 key 对 Redis 进行写操作。

  • 如果 key 存在,更新缓存
  • 如果 key 不存在,插入缓存
  • 4、 @CacheEvict 触发移除缓存

  • 根据 key 删除缓存中的数据。
  • 项目源码地址

    https://github.com/aalansehaiyang/spring-boot-bulking  

    模块:spring-boot-bulking-redis

    往期推荐

  • SpringBoot整合高性能微服务框架 gRPC

  • 还在用Mybatis? Spring Data JPA 让你的开发效率提升数倍!

  • Spring Boot 集成 ElasticSearch,实现高性能搜索

  • 框架扩展:注解 RPC Consumer属性动态注入

  • 淘宝订单自动确认收货的N种实现,秒杀面试官

  • 深入剖析优惠券核心架构设计

  • 某生鲜电商平台的库存扣减方案

  • 如何设计一个高性能的秒杀系统

  • 如何通过Binlog来实现不同系统间数据同步

  • 电商优惠券如何设计?

  • 单台 MySQL 支撑不了这么多的并发请求,我们该怎么办?

  • DDD是如何解决复杂业务扩展问题?

  • springboot + aop + Lua分布式限流的最佳实践

  • 最后编辑于
    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 203,362评论 5 477
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 85,330评论 2 381
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 150,247评论 0 337
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 54,560评论 1 273
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 63,580评论 5 365
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 48,569评论 1 281
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 37,929评论 3 395
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 36,587评论 0 258
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 40,840评论 1 297
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 35,596评论 2 321
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 37,678评论 1 329
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 33,366评论 4 318
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 38,945评论 3 307
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 29,929评论 0 19
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 31,165评论 1 259
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 43,271评论 2 349
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 42,403评论 2 342

    推荐阅读更多精彩内容