Redis
一、Redis命令
1.Redis数据结构
Redis是一个key-value的数据库,key一般是String类型,value的类型多种多样:
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的直连方式。
4.SpringDataRedis
(1)SpringDate时Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis。
(2)SpringDateRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
(3)SpringDataRedis使用步骤:
- 引入spring-boot-starter-data-redis依赖
- 在application.yml配置Redis信息
- 注入RedisTemplate
(4)SpringDataRedis的序列化方式:
RedisDataTemplate可以接受任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK的序列化。
缺点:
- 可读性差
- 内存占用较大
方案一:
- 自定义RedisTemplate
- 修改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的过程。
- 使用StringRedisTemplate
- 写入Redis时,手动把对象序列化为JSON
- 读取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)
业务场景:
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
(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.分布式锁的实现
分布式锁的核心是实现多进程将之间互斥,而满足这一点的方式有很多,常见的有三种:
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命令调用脚本常见命令:
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)发送消息的命令:
(3)读取消息的方式之一:XREAD
(4)XREAD阻塞方式,读取最新消息:
注意:指定起始ID为$时,代表读取最新消息,如果处理一条消息的过程中,又有超过一条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。
特点:
- 消息可回溯
- 一个消息可以被多个消费者读取
- 可以阻塞读取
- 有消息漏读的风险
(5)消费者组:将多个消费者划分到一个组中,监听同一个队列。特点:
- 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
- 消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取信息。确保每一个消息都会被消费
- 消息确认:消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除
(6)创建消费者组:
XFROUP CREATE key groupName ID [MKSTREAM]
- key:队列名称
- groupName:消费者组名称
- ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
- MKSTREAM:队列不存在时自动创建队列
其他常见命令:
(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消息队列
七、Feed流
1.关注推送
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
2.Feed六产品的两种常见模式
(1)Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
(2)智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户黏度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
3.Timeline实现方案
(1)拉模式:也叫做读扩散
(2)推模式:也叫做写扩散
(3)推拉结合:也叫做读写混合,兼具推和拉两种模式的优点
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统计来说,可以忽略。