Redis

一、Redis命令

1.Redis数据结构

Redis是一个key-value的数据库,key一般是String类型,value的类型多种多样:

image-20231030225220815

2.Redis通用命令

通用命令是部分数据类型的,都可以使用的指令,常见的有:

  • KEYS:查看符合模板的所有key,不建议在生产环境中使用

  • DEL:删除一个指定的key

  • EXISTS:判断key是否存在

  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

通过help [command] 可以查看一个命令的具体用法。

3.String类型

(1)String类型,也就是字符串类型,是Redis中最简单的存储类型。

其value是字符串,不过根据字符串的格式不同,又可以分为三类:

  • string:普通字符串
  • int:整数类型,可以做自增、自减操作
  • float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512m

(2)String类型的常见命令:

  • SET:田间或者修改已经存在的一个String类型的键值对
  • GET:根据key获取String类型的value
  • MSET:批量添加多个String类型的键值对
  • MGET:根据多个key获取多个String类型的value
  • INCR:让一个整型的key自增1
  • INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让nnum值自增2
  • INCRBYLOAT:让一个浮点类型的数字自增并指定步长
  • SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
  • SETEX:添加一个String类型的键值对,并且指定有效期

4.key的层级格式

Redis的key允许有多个单词形成层级结构,多个单词之间用 ' : ' 隔开,格式如下:

项目名:业务名:类型:id

这个规格并非固定,可以根据自己的需求来删除或添加词条。

如果value是一个java对象,则可以将对象序列化为JSON字符串后存储。

5.Hash类型

(1)Hash类型,也叫散列,其value是一个无序字典,类似于java中的HashMap结构。

String结构是将独享序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便。

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做crud。

(2)Hash类型的常见命令:

  • HSET key field value:添加或者修改hash类型key的field的值
  • HGET key field:获取一个hash类型key的field的值
  • HMSET:批量添加多个hash类型key的field的值
  • HMGET:批量获取多个hash类型key的field的值
  • HGETALL: 获取一个hash类型key中的所有field和value
  • HKEYS:获取一个hash类型key中的所有field
  • HGETS:获取一个hash类型key中的所有value
  • HINCRBY:让一个Hash类型key的字段值自增并指定步长
  • HSETNX:添加一个hash类型key的field的值,前提是这个field不存在,否则不执行

6.List类型

(1)Redis中的List类型与java中的LinkedList类似,可以看作是一个双向链表结构。即可以支持正向检索也可以支持反向检索。

特征也与LinkedList类似:

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

(2)List类型的常见命令:

  • LPUSH key element ... :向列表左侧插入一个或多个元素
  • LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil
  • RPUSH key element ... :向列表右侧插入一个或多个元素
  • RPOP key:移除并返回列表右侧的第一个元素,没有则返回nil
  • LRANGE key start end:返回一段角标范围内的所有元素
  • BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

7.set类型

(1)Redis的Set结构与Java中的HashSet类似,可以看作是一个value为null的HashMap。因为也是一个hash表,因此具备与HahSet类似的特征:

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集功能

(2)Set类型的常见命令:

  • SADD key member ... :向set中添加一个或多个元素
  • SREM key member ... :移除set中的指定元素
  • SCARD key:返回set中元素的个数
  • SISMEMBER key member:判断一个元素是否存在于set中
  • SMEMBERS:获取set中的所有元素
  • SINTER key1 key2 ... :求key1与key2的交集
  • SDIFF key1 key2 ... :求key1与key2的差集
  • SUNION key1 key2 ... :求key1和key2的并集

8.SortedSet类型:

(1)Redis的SortedSet是一个可排序的set集合,与java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkioList)加hash表。SortedSet具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快

因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

(2)SortedSet类型的常见命令:

  • 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范围内的元素
  • ZDIFF、ZINTER、ZUNION:求差集、交集、并集

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

二、Redis的Java客户端

1.客户端对比

(1)Jedis:以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis是线程不安全的,多线程环境下需要基于连接池来使用。

(2)lettuce:Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。

(3)Redisson:Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、Semaphore、AtomicLong等强大功能。

(4)java-redis-client

(5)vertx-redis-client

2.Jedis使用的基本步骤:

(1)引入依赖

(2)创建Jedis对象,建立连接

(3)使用Jedis,方法名与Redis命令一致

(4)释放资源

3.Jedis连接池

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,可以使用Jedis连接池代替Jedis的直连方式。

image-20231101222853500

4.SpringDataRedis

(1)SpringDate时Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis。

(2)SpringDateRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:

image-20231101224004107

(3)SpringDataRedis使用步骤:

  1. 引入spring-boot-starter-data-redis依赖
  2. 在application.yml配置Redis信息
  3. 注入RedisTemplate

(4)SpringDataRedis的序列化方式:

​ RedisDataTemplate可以接受任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK的序列化。

缺点:

  • 可读性差
  • 内存占用较大

​ 方案一:

  1. 自定义RedisTemplate
  2. 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
@Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        //创建Template
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        //设置连接工厂
        template.setConnectionFactory(connectionFactory);
        //设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //key和hashKey采用string序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashValueSerializer(RedisSerializer.string());
        //value和hashValue采用JSON序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        return template;
    }

​ 为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。

​ 方案二:

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

​ Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了自定义Template的过程。

  1. 使用StringRedisTemplate
  2. 写入Redis时,手动把对象序列化为JSON
  3. 读取Redis时,手动把读取到的JSON反序列化为对象
@Autowired
    private StringRedisTemplate stringRedisTemplate;
    //JSON工具
    private static final ObjectMapper mapper = new ObjectMapper();
    @Test
    public void testSaveUser() throws JsonProcessingException {
        //准备对象
        User user = new User("虎哥", 21);
        //手动序列化
        String s = mapper.writeValueAsString(user);
        //写入一条数据到Redis
        stringRedisTemplate.opsForValue().set("user:200",s);
        //读取数据
        String o =  stringRedisTemplate.opsForValue().get("user:200");
        //反序列化
        mapper.readValue(o,User.class);
        System.out.println("o = "+o);
    }

三、缓存

1.什么是缓存

(1)缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。

(2)缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

(3)缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

2.缓存更新策略

(1)

image-20231108171210793

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

(2)主动更新策略:

  • Cache Aside Pattern:由缓存的调用者,在更新数据库的同时更新缓存

    读:缓存命中则直接返回;缓存未命中则查询数据库,并写入缓存,设定超时时间

    写:先写数据库,再删除缓存;要确保数据库与缓存操作的原子性

  • Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题

  • Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致

3.缓存穿透

(1)缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

(2)常见的解决方案:

  • 缓存空对象

    优点:实现简单,维护方便

    缺点:额外的内存消耗;可能造成短期的不一致

  • 布隆过滤

    优点:内存占用较少,没有多余key

    缺点:实现复杂;存在误判可能

4.缓存雪崩

(1)缓存雪崩是指在同一时间段大量的缓存key同时失效或者服务器宕机,导致大量请求到达数据库,带来巨大压力。

(2)解决方案:

  • 给不同的key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

5.缓存击穿

(1)缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

(2)解决方案:

  • 互斥锁

    优点:没有额外的内存消耗;保持一致性;实现简单

    缺点:线程需要等待,性能受影响;可能有死锁风险

  • 逻辑过期

    优点:线程无需等待,性能较好

    缺点:不保证一致性,有额外内存消耗;实现复杂

四、秒杀

1.全局ID生成器

(1)全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性:

  • 唯一性
  • 高可用
  • 高性能
  • 递增性
  • 安全性

(2)全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

(4)Redis自增ID策略:

  • 每天一个key,方便统计订单量

  • 为了增加ID的安全性,可以不直接使用Redis自增的数值,而是拼接一些其他信息:

    符号位:1bit,永远为0;

    时间戳:31bit,以秒为单位,可以使用69年;

    序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID。

2.超卖问题

(1)超卖问题是典型的多线程安全问题,针对这一子问题的常见解决方式就是加锁:

  • 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。(Synchronized、Lock都属于悲观锁)

    优点:简单粗暴

    缺点:性能一般

  • 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据是去判断有没有其他线程对数据做了修改

    如果没有修改则认为自己是安全的,自己才更新数据;

    如果已经被其他线程修改说明发生了线程安全问题,此时可以重试或异常。

    优点:性能好

    缺点:存在成功率低的问题

(2)乐观锁判断之前查询得到的数据是否有被修改过,常见方式:

  • 版本号法
  • CAS法

五、分布式锁

1.什么是分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

特性:

  • 多进程可见
  • 互斥
  • 高可用
  • 高性能
  • 安全性

2.分布式锁的实现

分布式锁的核心是实现多进程将之间互斥,而满足这一点的方式有很多,常见的有三种:

image-20231114212744429

3.基于Redis的分布式锁

(1)实现分布式锁需要实现的两个基本方法:

  • 获取锁:

    互斥:确保只能有一个线程获取锁;

    非阻塞:尝试一次,成功返回true,失败返回false

  • 释放锁:

    手动释放;

    超时释放:获取锁时添加一个超时时间

(2)优化:

基于setnx实现的分布式锁存在下面问题:

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试:获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主机宕机时,如果从并同步中的锁数据,则会出现锁实现

4.Redis的Lua脚本

(1)Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,官网:https://www.runoob.com/lua/lua-tutorial.html

(2)Redis提供的调用函数:

redis.call('命令名称','key','其他参数',...)

(3)Redis命令调用脚本常见命令:

image-20231115140943688

5.Redisson

​ Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。官网:https://redissom.org

六、Redis消息队列

1.什么是消息队列

(1)消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括三个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

(2)Redis提供了三种不同的方式来实现消息队列:

  • List结构:基于List结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型

2.基于List结构模拟消息队列

(1)Redis的List数据结构是一个双向链表,很容易模拟出队列效果

(2)当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。应该使用BRPOP或者BLPOP来实现阻塞效果。

(3)优点:

  • 利用Redis存储,不受限于JVM内存上限
  • 基于Redis的持久化机制,数据安全性有保证
  • 可以满足消息有序性

(4)缺点:

  • 无法避免消息丢失
  • 只支持但消费者

3.基于PubSub(发布订阅)的消息队列

(1)PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。

  • SUBSCRIBE channel [channel]:订阅一个或多个频道
  • PUBLISH channel msg:向一个频道发送消息
  • PSUBSCRIBE pattern(pattern):订阅与pattern格式匹配的所有频道

(2)优点:采用发布订阅模型,支持多生产、多消费

(3)缺点:

  • 不支持数据持久化、
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失

4.基于Stream的消息队列

(1)Stream是Redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列

(2)发送消息的命令:

image-20231121195330030

(3)读取消息的方式之一:XREAD

image-20231121195710913

(4)XREAD阻塞方式,读取最新消息:

image-20231121200707021

注意:指定起始ID为$时,代表读取最新消息,如果处理一条消息的过程中,又有超过一条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

特点:

  • 消息可回溯
  • 一个消息可以被多个消费者读取
  • 可以阻塞读取
  • 有消息漏读的风险

(5)消费者组:将多个消费者划分到一个组中,监听同一个队列。特点:

  • 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
  • 消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取信息。确保每一个消息都会被消费
  • 消息确认:消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除

(6)创建消费者组:

XFROUP CREATE key groupName ID [MKSTREAM]

  • key:队列名称
  • groupName:消费者组名称
  • ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
  • MKSTREAM:队列不存在时自动创建队列

其他常见命令:

image-20231121211801205

(7)从消费者组读取消息

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]

  • group:消费组名称
  • consumer:消费者名称。如果消费者不存在,会自动创建一个消费者
  • coumt:本次查询的最大数量
  • BLOCK milloseconds:当没有消息时最长等待时间
  • NOACK:无需手动ACK,获取到消息后自动确认
  • STREAM key:指定队列名称
  • ID:获取消息的起始ID
    • ">":从下一个未消费的消息开始
    • 其他:根据指定id从pending-list中获取以消费单位确认的消息,例如0,是从pending-list中的第一个消息开始

XREADGROUP特点:

  • 消息可回溯
  • 可以多消费者挣抢消息,加快消费速度
  • 可以阻塞读取
  • 没有消息漏读的风险
  • 有消息确认机制,保证消息至少被消费一次

5.Redis消息队列

image-20231121214718306

七、Feed流

1.关注推送

关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

2.Feed六产品的两种常见模式

(1)Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

  • 优点:信息全面,不会有缺失。并且实现也相对简单
  • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

(2)智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

  • 优点:投喂用户感兴趣信息,用户黏度很高,容易沉迷
  • 缺点:如果算法不精准,可能起到反作用

3.Timeline实现方案

(1)拉模式:也叫做读扩散

(2)推模式:也叫做写扩散

(3)推拉结合:也叫做读写混合,兼具推和拉两种模式的优点

image-20231125150357804

4.Feed流的分页问题

Feed中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式

八、GEO数据结构的基本用法

1.GEO数据结构

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。

2.常见命令

(1)GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)

(2)GEODIST:计算指定的两个点之间的距离并返回

(3)GEOHASH:将指定的member的坐标转为hash字符串形式并返回

(4)GEOPOS:返回指定member的坐标

(5)GEOREDIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2后已废弃

(6)GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2新功能

(7)GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。6.2新功能

九、BitMap用法

1.BitMap

​ 把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为位图(BitMap)。Redis中是利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位。

2.BitMap用法

(1)SETBIT:向指定位置(offset)存入一个0或1

(2)GETBIT:获取指定位置(offset)的bit值

(3)BITCOUNT:统计BitMap中值为1的bit位的数量

(4)BITFIELD:操作(查询、修改、自增)BitMap中bie数组中的指定位置(offset)的值

(5)BITFIELD_RO:获取BitMap中bit数组,并以十进制形式返回

(6)BITOP:将多个BitMap的结果做位运算(与、或、异或)

(7)BITPOS:查找bit数组中指定范围内第一个0或1出现的位置

十、HyperLogLog

1.UV

全称Unique Vistor,也叫独立访问量,是指通过互联网访问、浏览这个网页的自然人。一天内同一个用户多次访问该网站,只记录一次。

2.PV

全称Page View,也叫页面访问量或点击量,用户访问网站的一个页面,记录一次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

3.用法

HyperLogLog(HLL)是从LogLog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。

Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用很低,但是有小于0.81%的误差,对于UV统计来说,可以忽略。

image-20231129205623645

posted @ 2024-03-18 20:45  __YJC  阅读(9)  评论(0编辑  收藏  举报