Redis使用规范

  • 一、redis 各框架对比与选型
  • 二、 uwork-starters-redis、uwork-starters-redis说明
  • 三、redis key 命名规范
  • 四、redis 键值序列化规范
  • 五、redis 数据类型使用选择规范
  • 六、一些需要注意的问题
    • 查询
    • 缓存穿透
    • 缓存击穿
    • 缓存雪崩
  • 七、各业务系统替换流程

一、redis 各框架对比与选型

项目

概述

性能

优点

缺点

Jedis

1)是Redis的Java实现的客户端,提供了比较全面的Redis命令的支持。
2)支持基本的数据类型如:String、Hash、List、Set、Sorted Set。
3)Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。

使用阻塞的I/O,方法调用同步,程序流需要等到socket处理完I/O才能执行,不支持异步操作。Jedis客户端实例不是线程安全的,需要通过连接池来使用Jedis。

1)提供了比较全面的提供了Redis的操作特性
2)学习成本较低
3)方法灵活可以自由组合

1)不支持读写分离,需要自己实现
2)使用了BIO模型,方法调用是同步的
3)jedis客户端实例不是线程安全的,需要使用连接池来使用

Redisson

1)实现了分布式和可扩展的Java数据结构。
2)Redisson不仅提供了一系列的分布式Java常用对象,基本可以与Java的基本数据结构通用,还提供了许多分布式服务,其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)

基于Netty框架的事件驱动的通信层,其方法调用是异步的。

1)Redisson的宗旨是促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。在分布式开发中,Redisson可提供更便捷的方法。
2)Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。

1)功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。
2)Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。

lettuce

高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。主要在一些分布式缓存框架上使用比较多。

基于Netty框架的事件驱动的通信层,其方法调用是异步的。

1)不需要考虑线程池,性能比较高
2)Spring生态默认
3)api是线程安全的,单个连接可以完成多个操作
4)同样支持连接池

 

 

上表为Jedis、Redisson、Lettuce之间的区别与优劣


    我目前使用的阿里云Redis服务,在我们对接使用过程中,实际集群相关的内容对于我们是透明的。在我们使用的过程中,可以理解为使用的是redis单节点服务,至于集群服务保障(节点监控、故障转移、主从复制)可以不关心。
就实际使用情况而言,目前对于redis缓存的使用量并不是很大(使用量为10%左右),故本次 采用 spring-boot-starter-data-redis 为主要框架,其在 springboot2.x之后,底层采用 lettuce 实现,在其之上封装给我们使用。对于分布式锁的实现,引入了 redisson 的模块,同时并存了自己实现的分布式锁,详细见下面的讲解

二、 uwork-starters-redis、uwork-starters-redis说明

uwork-starters-redis与uwork-starters-redisson只作为依赖管理和Bean装配工具

tech.uwork.common.util.redis作为功能提供方,目前有以下接口和工具类

DefaultLockUtil 使用redisTemplate实现分布式锁工具类
RedissonLockUtil Redisson分布式锁部分API工具类
IRedisTemplateService redisTemplate功能接口
IStringRedisTemplateService stringRedisTemplate功能接口

三、redis key 命名规范

【强制】
1)以英文字母开头,命名中只能出现小写字母、数字、英文点号(.)和英文半角冒号(:);
2)不要包含特殊字符,如下划线、空格、换行、单双引号以及其他转义字符;
【建议】
1)字符串长度不要超过39个字节
Redis存储STRING的内部编码: int(8字节长度整型)embstr(<=39字节)raw(>39字节)
RedisObject 的长度是 16 字节,SDS 的长度是 9 + 字符串长度。
因此当字符串长度是 39 时,embstr 的长度正好是 16+9+39=64,jemalloc 正好可以分配 64 字节的内存单元。避免了创建时二次分配空间,销毁时二次释放空间。
2)命名规范:业务模块名:业务逻辑含义:其他:value类型

  • 业务模块名:具体的功能模块
  • 业务逻辑含义:不同业务逻辑含义使用英文半角冒号(:)分割,同一业务逻辑含义段的单词之间使用英文半角点号 (.)分割,用来表示一个完整的语义
  • value类型:Redis key命名以key所代表的value类型结尾,以提高可读性;

例:user:wechat.token:{userId}:string

String key = "user:wechat:token:12345678901:string";

 

System.out.println(a.getBytes().length); // 36字节

 

四、redis 键值序列化规范


redis 序列化在使用 spring-boot-starter-data-redis 时,默认的是 JdkSerializationRedisSerializer。

  • JdkSerializationRedisSerializer 存在的问题:要求对象必须实现 Serializable 接口,静态属性不能序列化,存储为二进制数据,对开发者调试不友好
  • OxmSerializer:以xml格式存储(但还是String类型~),解析起来也比较复杂,效率也比较低。
  • StringRedisSerializer:也是StringRedisTemplate默认的序列化方式,key和value都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。
  • GenericToStringSerializer:需要调用者给传一个对象到字符串互转的Converter(相当于转换为字符串的操作交给转换器去做)使用起来其比较麻烦。
  • Jackson2JsonRedisSerializer:以Json的形式存储,效率高且对调用者友好。速度快,序列化后的字符串短小精悍,不需要实Serializable接口。缺点也非常致命:那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。在序列化的时候可以序列化自定义对象,但是反序列化的时候会出现问题。
  • GenericJackson2JsonRedisSerializer:基本和上面的Jackson2JsonRedisSerializer功能差不多。但是反序列化时不需要指定class对象,因为使用 GenericJackson2JsonRedisSerializer 进行序列化时保存了class相关的信息;但是在反序列化带泛型的List、Set等时,会报出类型转换失败。需要通过手动转换类型来解决。虽然 GenericJackson2JsonRedisSerializer 提供了通用的方式来解决 Jackson2JsonRedisSerializer 反序列化时需要class信息的问题,但是其效率远远要比 Jackson2JsonRedisSerializer 差,并且占用内存相对多,因此不建议使用。注,在泛型集合中,反序列化时会将数值类型自动理解为Integer和Double,会存在数据类型不匹配的风险。

因此结合上述说明,推荐对 Key 使用 StringRedisSerializer 进行序列化,对 value 使用 Jackson2JsonRedisSerializer 进行序列化。【注】以上均无法反序列化非静态内部类实例对象。

五、redis 数据类型使用选择规范

string

  • 作为key,尽量确保其在39字节之内;

  • 作为value,不建议存放对象;

list

  • 当做数组、队列、栈使用;

  • 列表元素数量小于512个时,并且没有经常增删改的需求,则尽量保证每个元素大小不超过64字节;

hash

  • 适用于存放对象;

set

  • 无序不重复集合;

  • 可使用交集、并集、差集等操作;

zset

  • 有序不重复集合;

六、一些需要注意的问题

查询

禁止使用keys *

Set set = new Set();

int startCursot = 0;

int cursot = 0;

while(cursot != startCursot){

    cursot = invoke.scan cursot [MATCH pattern] [COUNT count]

    set.addAll( invoke.scan cursor [MATCH pattern] [COUNT count] );

}

 

缓存穿透

问题描述:
一般套路是先查询缓存,缓存中不存在,再去查询数据库。数据库查询到后,会进行缓存,下次就可在缓存中命中。当数据库中也没有该数据,会返回null等,这样就进行了两次无效的查询。
方案:
简单粗暴的,即便数据库查询为null,也进行缓存,但是该key建议设置较短的过期时间(<=5分钟)。

缓存击穿

问题描述:
当一个热点key过期,同时会有大量并发请求到数据库,同时需要建立新的缓存
方案:
提前知道热点数据,不设置过期;
提前不知道会演变成热点数据,当重建缓存比较简单,考虑互斥锁进行缓存重建;当重建缓存比较复杂,考虑使用逻辑过期时间,即不真正设置过期键,而是设置一个逻辑过期时间,当系统判断达到逻辑过期时间时,重建缓存;

缓存雪崩

问题描述:
当同一时刻大量缓存同时过期,导致后台服务压力骤然增加
方案:
在缓存过期时间设置的时候,可以采用 固定 + 随机 = 过期时间;
同时,如果内存压力不大,可以采用缓存标记方法,设置 同时,设置 。time1 是 time2 的2倍长或更大,当程序获取 key时,同时获取 key + "_sign",当缓存标记存在,则返回 value ,当不存在,则使用异步重建该缓存。

你可能感兴趣的:(redis,java)