后端-redis

Redis

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:String、Hash、List、Set、SortedSet

String类型

String类型,也就是字符串类型,是Redis中最简单的存储类型。底层是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512M。value是字符串,不过根据字符串的格式不同,又可以分为:

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

String类型的常用命令

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

Hash类型

Hash类型,也叫散列,其value是一个无序字典,类似于java中的HashMap结构。
String结构是将对象序列化为json字符串后存储,当需要修改对象某个字段时很不方便。
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD。

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
  • HVALS:获取一个hash类型的key中的所有的value
  • HINCRBY:让一个hash类型key的字段值自增并指定步长
  • HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

List类型

Redis中的List类型与java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。特征也与LinkedList类似:

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

List类型的常用命令

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

Set类型

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

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

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的并集

SortedSet类型

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

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

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:让scored set中的指定元素自增,步长为指定的increment值
  • ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
  • ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
  • ZDIFF、ZINTER、ZUNION:求差集、交集、并集

Redis序列化

public RedisTemplate<String, Objec> redisTemplate(RedisConnectionFactory connectionFactory) {
    // 创建RedisTemplate对象
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    // 设置连接工厂
    template.setConnectionFactory(connectionFactory);
    // 创建JSON序列化工具
    GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    // 设置key的序列化
    template.setKeySerializer(RedisSerializer.string());
    template.setHashKeySerializer(RedisSerializer.string());
    // 设置value的序列化
    template.setValueSerializer(jsonRedisSerializer);
    template.setHashValueSerializer(jsonRedisSerializer);
    
    return template
}

缓存更新策略

  1. 低一致性需求:使用Redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
    1. 读操作:
      • 缓存命中则直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    2. 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。常见的解决方案有两种:

  1. 缓存空对象

    • 优点:实现简单,维护方便
    • 缺点:额外的内存消耗;可能造成短期的不一致
  2. 布隆过滤

    • 优点:内存占用较少,没有多余key
    • 缺点:实现复杂;存在误判可能
  3. 增强id的复杂度,避免被猜测id规律

  4. 做好数据的基础格式校验

  5. 加强用户权限校验

  6. 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。解决方案:

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

缓存击穿

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

  • 互斥锁

  • 逻辑过期

    优点缺点
    互斥锁没有额外的内存消耗;保证一致性;实现简单线程需要等待,性能受影响;可能有死锁风险
    逻辑过期线程无需等待,性能较好不保证一致性;有额外内存消耗;实现复杂

超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁。

  1. 悲观锁
    认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如synchronized、lock都属于悲观锁
  • 优点:简单粗暴
  • 缺点:性能一般
  1. 乐观锁
    认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改
    (1)如果没有修改则认为是安全的,自己才更新数据
    (2)如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常
  • 优点:性能好
  • 缺点:存在成功率低的问题
// 获取代理对象事务
父类 对象 = (子类)AopContext.currentProxy();
return 对象.当前对象的方法 // 比如this.方法

lua脚本

lua脚本的写法

-- 比较线程标示与锁中的标志是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
	-- 释放锁 del key
	return redis.call('del', KEYS[1])
end
return 0

Java的代码调用

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
</dependency>
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
	UNLOCK_SCRIPT = new DefaultRedisScript<>();
	UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
	UNLOCK_SCRIPT.setResultType(Long.class);
}
strigRedisTemplate.execute(UNLOCK_SCRIPT, 脚本的参数)

基于Redis的分布式锁优化

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

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

Redisson

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

  • 可重入锁
  • 公平锁
  • 联锁
  • 红锁
  • 读写锁
  • 信号量
  • 可过期性信号量
  • 闭锁

Redisson入门

  1. 引入依赖
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>
  1. 配置Redisson客户端
@Configuration
public class RedisConfig {
	@Bean
	public RedissonClient redissonClient() {
		// 配置类
		Config config = new Config();
		// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
		config.useSingleServer().setAddress("redis://192.168.159.101:6379").setPassword("123321");
		// 创建客户端
		return Redisson.create(config);
	}
}
  1. 使用Redisson的分布式锁
@Resource
private RedissonClient redissonClient;

@Test
void testRedisson() throws InterruptedException {
	// 获取锁(可重入),指定锁的名称
	RLock lock = redissonClient.getLock("anyLock");
	// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
	boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
	// 判断释放获取成功
	if (isLock) {
		try {
			System.out.println("执行业务");
		}finally {
			// 释放锁
			lock.unlock();
		}
	}
}

Redisson可重入锁原理

获取锁的Lua的脚本

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if (redis.call('exists', key) == 0) then
	-- 不存在,获取锁
	redis.call('hset', key, threadId, '1');
	-- 设置有效期
	redis.call('expire', key, releaseTime);
	return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if (redis.call('hexists', key, threadId) == 1) then
	-- 不存在,获取锁,重入次数+1
	redis.call('hincrby', key, threadId, '1');
	-- 设置有效期
	redis.call('expire', key, releaseTime);
	return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的Lua脚本

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
	return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断是否重入次数是否已经为0
if (count > 0) then
	-- 大于0说明不能释放锁,重置有效期然后返回
	redis.call('EXPIRE', key, releaseTime);
	return nil;
else -- 等于0说明可以释放锁,直接删除
	redis.call('DEL', key);
	return nil;
end;

总结
1. 不可重入Redis分布式锁

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入、无法重试、锁超时失效

2. 可重入的Redis分布式锁

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
  • 缺陷:redis宕机引起锁失效问题

3. Redisson的multiLock

  • 原理:多个独立的redis节点,必须在所有节点都获取重入锁,才算获取锁成功

Redis异步秒杀

lua脚本

-- 优惠券id
local voucherId = ARGV[1]
-- 用户id
local userId = ARGV[2]

-- 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 判断库存是否充足
if(tonumber(redis.call('get', stockKey)) <= 0) then
	-- 库存不足,返回1
	return 1
end
-- 判断用户是否下单SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
	-- 存在,说明是重复下单,返回2
	return 2
end
-- 扣库存incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)

Redis消息队列

消息队列,字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:

  • 消息队列:存储和管理信息,也被称为消息代理
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息
    Redis提供了三种不同的方式来实现消息队列:
  • list结构:基于List结构模拟消息队列
  • PubSub:基本的点对点消息模型
  • Stream:比较完善的消息队列模型

基于List结构模拟消息队列

消息队列,字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果
队列是入口和出口不在一边,因此我们可以利用:LPUSH结合RPOP、或者RPUSH结合LPOP来实现
不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息
因此这里应该使用BRPOP或者BLPOP来实现阻塞效果
优点:

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

缺点:

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

基于PubSub结构模拟

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

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

优点:

  • 采用发布订阅模型,支持多生产、多消费

缺点:

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

基于Stream的消息队列

Stream是Redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列
发送消息的命令:

XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|ID field value [field value ...]
  • NOMKSTREAM:如果队列不存在,是否自动创建队列。默认是自动创建
  • [MAXLEN|MINID [=|~] threshold [LIMIT count]]:设置消息队列的最大消息数量
  • ID:消息的唯一id,*代表由Redis自动生成,格式是“时间戳-递增数字”
  • field value [field value …]:发送到队列中的消息,称为Entry。格式是多个key-value键值对

读取消息的命令:

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
  • [COUNT count]:每次读取消息的最大数量
  • [BLOCK milliseconds]:当没有消息时,是否阻塞,阻塞时长
  • STREAMS key [key …]:要从哪个队列读取消息,key就是队列名
  • ID:起始id,只返回大于该ID的消息。0代表从第一个消息开始;$:代表从最新的消息开始

Stream类型消息队列的xread命令特点:

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

基于Stream的消息队列-消费者组

消费者组:将多个消费者划分到一个组中,监听同一个队列

  • 消息分流

    队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度

  • 消息标示

    消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费

  • 消息确认

    消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除

创建消费者组:

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

其它常见命令:

# 删除指定的消费者组
XGROUP DESTORY key groupName
# 给指定的消费者组添加消费者
XGROUP CREATECONSUMER key groupname consumername
# 删除消费者组中的指定消费者
XGROUP DELCONSUMER key groupname consumername

从消费者组读取消息:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group:消费者名称
  • consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
  • count:本次查询的最大数量
  • BLOCK milliseconds:当没有消息时最长等待时间
  • NOACK:无需手动ACK,获取到消息后自动确认
  • STREAMS key:指定队列名称
  • ID:获取消息的起始ID:
    • ”>“:从下一个未消费的消息开始
    • 其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始

Stream类型消息队列的XREADGROUP命令特点:

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

Redis消息队列

ListPubSubStream
消息持久化支持不支持支持
阻塞读取支持支持支持
消息堆积处理受限于内存空间,可以利用多消费者加快处理受限于消费者缓冲区受限于队列长度,可以利用消费者组提高消费速度,减少堆积
不支持不支持支持
不支持不支持支持

关注推送

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

Feed流的模式

Feed流产品有两种常见模式:

  • Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
    • 优点:信息全面,不会有缺失。并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
    • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
    • 缺点:如果算法不精准,可能起到反作用

Feed流的实现方案

拉模式推模式推拉结合
写比例
读比例
用户读取延迟
实现难度复杂简单很复杂
使用场景很少使用用户量少、没有大V过千万的用户量,有大V

Java的用法

// 查询收件箱
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2)

GEO数据结构

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

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。6.2新功能

SpringDataRedis的2.3.9版本并不支持Redis6.2提供的GEOSEARCH命令,因此我们需要提示其版本,修改自己的POM文件

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<exclusion>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
		</exclusion>
		<exclusion>
            <groupId>lettuce-core</groupId>
            <artifactId>io.lettuce</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.6.2</version>
</dependency>
<dependency>
    <groupId>lettuce-core</groupId>
    <artifactId>io.lettuce</artifactId>
    <version>6.1.6.RELEASE</version>
</dependency>

Java的用法

// 按照距离排序、分页
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key, GeoReference.fromCoordinate(x, y), new Distance(5000), RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
// 截取from - end 的部分
list.stream().skip(from).forEach() //...忽略

// 导入数据到Geo
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
for() {
    locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));
}

stringRedisTemplate.opsForGeo().add(key, locations);

BitMap

Redis中是利用string类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位。BitMap的操作命令有:

  • SETBIT:向指定位置(offset)存入一个0或1
  • GETBIT:获取指定位置(offset)的bit值
  • BITCOUNT:统计BitMap中值为1的bit位的数量
  • BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
  • BITFIELD_RO:获取BitMap中bit数组,并以十进制形式返回
  • BITOP:将多个BitMap的结果做位运算(与、或、异或)
  • BITPOS:查找bit数组中指定范围内第一个0或1出现的位置

Java的用法

// 月签到
stringRedisTemplate.opsForValue().setBit(key, LocalDateTime.now().getDayOfMonth() - 1, true);
// 获取本月截止今天为止的所有签到记录
stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(LocalDateTime.now().getDayOfMonth())).valueAt(0));

RDB持久化

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

快照文件成为RDB文件,默认是保存在当前运行目录

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

# 900秒内,如果至少有1个key被修改,则执行bgsave,如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000

RDB的其它配置也可以在redis.conf文件中设置:

# 是否压缩,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes

# RDB文件名称
dbfilename dump.rdb

# 文件保存的路径目录
dir ./

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件

fork采用的是copy-on-write技术

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

AOF持久化

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲去数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项刷盘时机优点缺点
always同步刷盘可靠性高,几乎不丢数据性能影响大
everysec每秒刷盘性能适中最多丢失1秒数据
no操作系统控制性能最好可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:

# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

RDB和AOF比较

RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用

RDBAOF
持久化方式定时对整个内存做快照记录每一次执行的命令
数据完整性不完整,两次备份之间会丢失相对完整,取决于刷盘策略
文件大小会有压缩,文件体积小记录命令,文件体积很大
宕机恢复速度很快
数据恢复优先级低,因为数据完整性不如AOF高,因为数据完整性更高
系统资源占用高,大量CPU和内存消耗低,主要是磁盘IO资源,但AOF重写时会占用大量CPU和内存资源
使用场景可以容忍数分钟的数据丢失,追求更快的启动速度对数据安全性要求较高常见

数据同步原理

全量同步

  • slave节点执行replicaof命令第一次连接请求增量同步
  • master节点判断replid,发现不一致,拒绝增量同步
  • master将完整内存数据执行bgsave生成RDB,发送RDB到slave
  • slave清空本地数据,加载master的RDB
  • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
  1. Replication Id: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会集成master节点的replid
  2. offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新

因此slave做数据同步,必须向master声明自己的replication idoffset,master才可以判断到底需要同步哪些数据

增量同步

主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步

  • slave重启,请求增量同步
  • master节点判断replid,发现一致,回复continue
  • master去repl_baklog中获取offset后的数据,发送offset后的命令给slave

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步

Redis主从集群优化:

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步

Redis哨兵

哨兵的作用

Redis提供了哨兵机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:

  • 监控:Sentinel会不断检查你的master和slave是否按预期工作
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
服务状态监控

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每隔实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线
  • 客观下线:若超过指定数量的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半
选举新的master

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的运行id大小,越小优先级越高
如何实现故障转移

当选中了其中一个slave为新的master后,故障的转移的步骤如下:

  • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
  • sentinel给所有其它slave发送slaveof 192.168.150.101 7002命令,让这些slave成为新master的从节点,开始从新的master上同步数据
  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

RedisTemplate的哨兵模式

  1. 在pom文件中引入redis的starter依赖:

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 然后再配置文件application.yml中指定sentinel相关信息:

    spring:
    	redis:
    		sentinel:
    			master: mymaster  # 指定master名称
    			nodes: # 指定redis-snetinel集群信息
    				- 192.169.150.101:27001
    				- 192.169.150.101:27002
    				- 192.169.150.101:27003
    
  3. 配置主从读写分离

    @Bean
    public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer() {
        return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }
    

    这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:

    • MASTER:从主节点读取
    • MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
    • REPLICA:从slave(replica)节点读取
    • REPLICA_PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master

分片集群

分片集群结构

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有多个master,每隔master保存不同数据
  • 每隔master都可以有多个slave节点
  • master之间通过ping检测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

散列插槽

Redis会把每一个master节点映射到0~16384个插槽上,查看集群信息时就能看到:

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:

  • key中包含“{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
  • key中不包含“{}”,整个key都是有效部分

例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值

多级缓存

进程缓存

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。

  1. 分布式缓存(Redis)
    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  2. 进程本地缓存(HashMap、GuavaCache)
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据较小

Caffeine

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine

@Test
void test() {
    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder().build();
    
    // 存数据
    cache.put(key, value);
    
    // 取数据,不存在则返回
    String value = cache.getIfPresent(key);
    
    // 取数据,不存在则去数据库查询
    String value = cache.get(key, key -> {
        // 查询数据库
    });
}

Caffeine提供了三种缓存驱逐策略

  • 基于容量:设置缓存的数量上限

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1)   // 设置缓存大小上限为1
        .build();
    
  • 基于时间:设置缓存的有效时间

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(Duration.ofSeconds(10))  // 设置缓存有效期为10秒,从最后一次写入开始计时
        .build();
        
    
  • 基于引用:设置缓存为软引用或若引用,利用GC来回收缓存数据。性能较差,不建议使用

在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐

Lua语法

数据类型

数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean包含两个值:false和true
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示
function由C或Lua编写的函数
tableLua中的表(table)其实是一个“关联数组”,数组的索引可以是数字、字符串或表类型。在Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{},用来创建一个空表

变量

Lua声明变量的时候,并不需要指定数据类型

-- 声明字符串
local str = 'hello'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true
-- 声明数组 key为索引的table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map = {name = 'Jack', age = 21}

访问table

-- 访问数组,lua数组的角标从1开始
print(arr[1])
-- 访问table
print(map['name'])
print(map.name)

循环

数组、table都可以利用for循环来遍历

  • 遍历数组

    -- 声明数组 key为索引的table
    local arr = {'java', 'python', 'lua'}
    -- 遍历数组
    for index, value in ipairs(arr) do
        print(index, value)
    end
    
  • 遍历table

    -- 声明map,也就是table
    local map = {name='Jack', age=21}
    -- 遍历table
    for key, value in pairs(map) do
        print(key, value)
    end
    

函数

定义函数的语法

function 函数名(argument1, argument2..., argument)
    -- 函数体
    return 返回值
end

例如,定义一个函数,用来打印数组

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

条件控制

类似java的条件控制,例如if、else语法

if(布尔表达式)
then
    -- [ 布尔表达式为 true 时执行该语句块 --]
else
    -- [ 布尔表达式为 false 时执行该语句块 -- ]
end

与java不同,布尔表达式中的逻辑运算是基于英文单词

操作符描述实例
and逻辑与操作符。若A为false,则返回A,否则返回B(A and B) 为 false
or逻辑或操作符。若A为true,则返回A,否则返回B(A or B) 为 true
not逻辑非操作符。与逻辑运算结果相反,如果条件为true,逻辑非为falsenot(A and B) 为 true

优雅的key结构

Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:

  • 遵循基本格式:[业务名称]:[数据名]:[id]
  • 长度不超过44字节
  • 不包含特殊字符

优点:

  • 可读性强
  • 避免key冲突
  • 方便管理
  • 更节省内存:key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小

BigKey问题

BigKey

BigKey通常以Key的大小和Key中成员的数量来综合判定

  • Key本身的数据量过大:一个String类型的Key,它的值为5MB
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100MB

建议:(1)单个key的value小于10KB;(2)对于集合类型的key,建议元素数量小于1000

BigKey的危害

  • 网络阻塞

    对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢

  • 数据倾斜

    BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡

  • Redis阻塞

    对元素较多的hash、list、zset等做运算会耗时较久,使主线程被阻塞

  • CPU压力

    对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用

发现BigKey

  • redis-cli --bigkeys

    利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回key的整体统计信息与每个数据的top1的big key

  • scan扫描

    自己编程,利用scan扫描redis中的所有key,利用strlen、hlen等命令判断key的长度

  • 第三方工具

    利用第三方工具,如redis-rdb-tools分析rdb快照文件,全面分析内存使用情况

  • 网络监控

    自定义工具,监控进出redis的网络数据,超出预警值时主动告警

删除BigKey

BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题

  • redis3.0及以下版本

    如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

  • redis4.0以后

    redis在4.0后提供了异步删除的命令:unlink

选择合适的数据结构

恰当的数据类型

  • json字符串(优点:实现简单粗暴;缺点:数据耦合,不够灵活)
  • 字段打散(优点:可以灵活访问对象任意字段;缺点:占用空间大、没办法做统一控制)
  • hash(优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段;缺点:代码相对复杂)
  • hash的entry数量超过500时,会使用哈希表而不是ziplist,内存占用较多,可以通过hash-max-ziplist-entries配置entry上限,但是如果entry过多就会导致BigKey问题
  • string结构底层没有太多内存优化,内存占用较多,想要批量获取这些数据比较麻烦

持久化配置

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  1. 用来做缓存的Redis实例尽量不要开启持久化功能
  2. 建议关闭RDB持久化功能,使用AOF持久化
  3. 利用脚本定期在slave节点做RDB,实现数据备份
  4. 设置合理的rewrite阈值,避免频繁的bgrewrite
  5. 配置no-appendfsync-on-rewrite=yes,禁止在rewrite期间做AOF,避免因AOF引起的阻塞

部署有关建议:

  1. Redis实例的物理机要预留足够内存,应对fork和rewrite
  2. 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
  3. 不要与CPU密集型应用部署在一起
  4. 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

慢查询

慢查询:在Redis执行时耗时超过某个阈值的命令,称为慢查询

慢查询的阈值可以通过配置指定:

  • slowlog-log-slower-than:慢查询阈值,单位是微秒。默认是10000,建议1000

慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:

  • slowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是128,建议1000

修改这两个配置可以使用:config set命令

查看慢查询日志列表:

  • slowlog len:查询慢查询日志长度
  • slowlog get [n]:读取n条慢查询日志
  • slowlog reset:清空慢查询列表

命令及安全配置

漏洞出现的核心的原因有以下几点:

  • Redis未设置密码
  • 利用了Redis的config set命令动态修改Redis配置
  • 使用了Root账号权限启动Redis

为了避免这样搞的漏洞,这里给出一些建议:

  • Redis一定要设置密码
  • 禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。可以利用rename-command禁用
  • bind:限制网卡,禁止外网网卡访问
  • 开启防火墙
  • 不要使用Root账户启动Redis
  • 尽量不是有默认的端口

内存配置

数据内存的问题

当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因

内存占用说明
数据内存是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题
进程内存Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;这部分内存大约几兆,在大多数生产环境中与Redis数据占用的内存相比可以忽略
缓冲区内存一般包括客户端缓冲区、AOF缓冲区、赋值缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出

Redis提供了一些命令,可以查看到Redis目前的内存分配状态

  • info memory
  • memory xxx

内存缓冲区配置

内存缓冲区常见的有三种:

  • 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1mb
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置client-output-buffer-limit

集群还是主从

集群最佳实践

集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:

  • 集群完整性问题
  • 集群带宽问题
  • 数据倾斜问题
  • 客户端性能问题
  • 命令的集群兼容性问题
  • lua和事务问题
集群完整性问题

在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

为了保证高可用特性,这里建议将cluster-require-full-coverage配置为false

集群带宽问题

集群节点之间会不断地互相ping来确定集群中其它节点的状态。每次ping携带的信息至少包括:

  • 插槽信息
  • 集群状态信息

集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高

  • 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群
  • 避免在单个物理机中运行太多Redis实例
  • 配置合适的cluster-node-timeout值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值