redis与分布式锁与缓存数据一致性问题浅谈
1.重要的概念
redis的8种数据结构
String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)
三种特殊类型 Geo(地理位置)、HyperLogLog(基数统计)、Bitmaps(位图)
Redis 的跳表(skiplist)是一种随机化的数据结构.Redis使用跳表作为有序集合(Zset)键的底层实现之一。
通过给集合(set)中的每个元素设定一个分数(score),这样集合中的成员可以根据分数进行从大到小的排序,这样的集合叫做有序集合(zset)
redis集群数据如何复制?
Redis集群的数据复制可以通过主从模式来完成。在Redis中,每个节点都有自己的角色,包括主节点(master)和从节点(slave)。当创建或添加新的节点到Redis集群时,默认情况下会将其设置为从节点并与现有的主节点进行连接。这样就形成了一个多主多从的结构。
主节点负责处理写操作,而从节点则用于复制主节点上的数据。
1.当主节点收到写命令后,首先会对该命令进行日志记录,
2.然后再将修改应用到内存中的数据库中
3. 同时,主节点还会向所有从节点发送相同的写命令,使得从节点也能保持最新的数据状态。
4.从节点在接收到主节点发送的写命令之前,需要等待主节点确认已经正常地将该命令写入本地的AOF文件或者RDB文件中。只有在主节点确认写入成功后,才会开始执行该写命令。因此,从节点必须严格按照主节点的指示来更新自身的数据。
数据写入--->主节点记录日志--->将修改应用到内存数据库--->主节点确认数据正常的写入本地AOF或RDB--->向从节点发送写命令--->从节点更新自身数据
Redis 为什么这么快?
1)纯内存结构、2)单线程、3)多路复用、4)数据结构(例如string的数据结构是SDS)
2.高并发下缓存失效问题
1.1 缓存穿透:
缓存穿透:
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:
null结果缓存,并加入短暂过期时间
1.2 缓存雪崩
缓存雪崩:
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
风险:
DB瞬时压力过重雪崩
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
1.3 缓存击穿
缓存击穿:
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
风险:
数据库瞬时压力增大,最终导致崩溃
解决:
加锁大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
3.分布式锁
分布式锁需要解决的问题:
问题1:
setnx占好了位,业务代码异常或者程序在页面过程中宕机,没有执行删除锁逻辑,这就造成了死锁
解决:
设置锁的自动过期,即使没有删除,会自动删除
问题2:
setnx设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis支持使用setnx ex命令
问题3:
删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除
问题4:
如果正好判断是当前值,正要删除锁的时候,锁已经过期, 别人已经设置到了新的值。那么我们删除的是别人的锁
解决:
删除锁必须保证原子性。使用redis+Lua脚本完成。 String script = “if redis.call(‘get’,
KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”;
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。 更难的事情,锁的自动续期
背景:最近遇到一个生产问题,分布式部署了十几条服务器,有个业务过期的定时任务会每天发邮件提醒用户,然而用户最近反馈,每天收到好几封提醒邮件,于是排查多发的原因。这个分布式锁很重要,大概率是没锁住
2.1 RedisTemplate(或stringRedisTemplate) 实现(方法一)
public void doSendEmail() {
//1.生成随机数
String uuid = UUID.randomUUID().toString();
//2.设置分布式锁,并设置过期时间120秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock_grant_expire_notice", uuid, 120, TimeUnit.SECONDS);
//3.获取到锁
try {
if (lock) {
//4.抢到锁把计数归零 RECURSIVE_CALL_TIMES 是定义在类下面的常量 private int RECURSIVE_CALL_TIMES = 0;
RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
//5.获取邮件是否已发送标识
Object isSendFlag = redisTemplate.opsForValue().get("mail_is_send_flag");
//6.没有值,就是未发送邮件
if (isSendFlag == null || StringUtils.isEmpty(isSendFlag)) {
//加锁成功...执行业务
//7.发送邮件的真正业务
//sendMail();
redisTemplate.opsForValue().set("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60, TimeUnit.SECONDS);
} else {
//8.邮件已发送,无需重复发送
log.info("mail already send,there's no need to send it twice");
}
} else {
//9.未获取到分布式锁,尝试自璇,每10秒递归调用一次,尝试获取分布式锁,最多尝试5次
Thread.sleep(10000);
if (RECURSIVE_CALL_TIMES <= 5) {
RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
doSendEmail();
}
}
} catch (Exception e) {
log.error("execute send mail fail,message:" + e);
}finally{
//10.释放分布式锁,对比uuid值是为了只删除自己的锁,且对比值和删锁是原子操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock_grant_expire_notice"), uuid);
}
}
2.2 JedisCluster 实现(方法二)
Jedis初始化类,连接redis,以及一些常用的方法
@Component("redisClusterConfig")
public class RedisClusterConfig {
private static Log log = LogFactory.getLog(RedisClusterConfig.class);
private volatile JedisCluster jedisCluster;
public RedisClusterConfig() {
initCluster();
}
public JedisCluster getClusterResource() {
initCluster();
return jedisCluster;
}
private void initCluster() {
if (null != jedisCluster) {
return;
}
synchronized (this) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//redis的相关配置文件(把redis的相关信息配置在一个文件中,读取文件)
Properties properties = PropertyUtils.loadProperty("redis-context.properties");
jedisPoolConfig.setMaxTotal(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxTotal", 50));
jedisPoolConfig.setMaxIdle(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxIdle", 20));
jedisPoolConfig.setMinIdle(PropertyUtils.getIntegerProperty(properties, "redis.pool.minIdle", 10));
jedisPoolConfig.setMaxWaitMillis(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxWaitMillis", 10000));
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(PropertyUtils.getIntegerProperty(properties, "redis.pool.timeBetweenEvictionRunsMills", 60000));
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(false);
jedisPoolConfig.setTestWhileIdle(true);
String clusterHost = properties.getProperty("redis.pool.cluster.host");
int timeout = PropertyUtils.getIntegerProperty(properties, "redis.timeout", 2000);
int sockettime = PropertyUtils.getIntegerProperty(properties, "redis.pool.cluster.sockettimeout", 2000);
int maxAttempts = PropertyUtils.getIntegerProperty(properties, "redis.pool.cluster.maxAttempts", 2000);
String password = properties.getProperty("redis.pool.cluster.password");
Set<HostAndPort> nodes = new HashSet<>();
String[] hosts = clusterHost.split(",");
for (String ipPort : hosts) {
String[] ipPortArr = ipPort.split(":");
String ip = ipPortArr[0];
int port = Integer.parseInt(ipPortArr[1]);
nodes.add(new HostAndPort(ip, port));
}
jedisCluster = new JedisCluster(nodes, timeout, sockettime, maxAttempts, password, jedisPoolConfig);
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (jedisCluster != null) {
jedisCluster.close();
}
}
/**
* 获取分布式锁
*
* @param lockKey
* @param value
* @param expireTime
* @return
*/
public boolean getLock(String lockKey, String value, int expireTime) {
String LOCK_SUCCESS = "OK";
boolean clusterRtnValue = false;
lockKey = replace4set(lockKey);
JedisCluster clusterResource = getClusterResource();
try {
String result = clusterResource.set(lockKey, value, "NX", "EX", expireTime);
if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
clusterRtnValue = true;
}
} catch (Exception e) {
log.error("exception:" + e);
}
return clusterRtnValue;
}
public boolean releaseLock(String lockKey, String value) {
Long RELEASE_SUCCESS = 1L;
boolean clusterRtnValue = false;
lockKey = replace4set(lockKey);
JedisCluster clusterResource = getClusterResource();
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = clusterResource.eval(script, Collections.singletonList(lockKey), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
clusterRtnValue = true;
}
} catch (Exception e) {
log.error("exception:" + e);
}
return clusterRtnValue;
}
public String set(String key, String value) {
String clusterRtnValue = null;
JedisCluster clusterResource = getClusterResource();
try {
clusterRtnValue = clusterResource.set(key, value);
} catch (Exception e) {
log.error("exception:" + e);
}
return clusterRtnValue;
}
public String setEx(String key, String value, int seconds) {
String clusterRtValue = null;
key = replace4set(key);
JedisCluster clusterResource = getClusterResource();
try {
clusterRtValue = clusterResource.setex(key, seconds, value);
} catch (Exception e) {
log.error("exception:" + e);
}
return clusterRtValue;
}
public String get(String key) {
String clusterRtValue = null;
key = replace4set(key);
JedisCluster clusterResource = getClusterResource();
try {
clusterRtValue = clusterResource.get(key);
} catch (Exception e) {
log.error("exception:" + e);
}
return clusterRtValue;
}
private String replace4set(String str) {
return str.replaceAll("\\{", "[").replaceAll("}", "]");
}
}
redis-context.properties配置内容
redis.pool.maxTotal=50
redis.pool.maxIdle=20
redis.pool.minIdle=10
redis.pool.maxWaitMillis=10000
redis.pool.timeBetweenEvictionRunsMills=60000
redis.pool.cluster.host=192.168.10.128:6379,192.168.10.131:6379
redis.timeout=2000
redis.pool.cluster.sockettimeout=2000
redis.pool.cluster.maxAttempts=2000
redis.pool.cluster.password=420188
分布式锁发送邮件
public void doSendEmail() {
//1.生成随机数
String redisLockValue = UUID.randomUUID().toString();
//2.设置分布式锁,并设置过期时间120秒
boolean lock = redisClusterConfig.getLock("lock_grant_expire_notice", redisLockValue, 120);
try {
if (lock) {
//抢到锁把计数归零
RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
//获取邮件是否已发送标识
String isSendFlag = redisClusterConfig.get("mail_is_send_flag");
//未发送
if (StringUtils.isBlank(isSendFlag)) {
//发送邮件的真正业务
//sendMail();
redisClusterConfig.setEx("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60);
} else {
//已发送
log.info("mail already send,there's no need to send it twice");
}
} else {
//未获取到分布式锁
Thread.sleep(10000);
if (RECURSIVE_CALL_TIMES <= 5) {
RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
doSendEmail();
}
}
} catch (Exception e) {
log.error("execute send mail fail,message:" + e);
}finally{
//释放分布式锁
redisClusterConfig.releaseLock("lock_grant_expire_notice", redisLockValue);
}
}
2.3 Redisson 实现(方法三)
以上两种方法都差不多,但无法解决redis续期问题,如果业务执行时间超过了分布式锁的过期时间,会有问题。当然 把分布式锁时间设置稍长一点一般也没什么大问题。redisson在业务未执行完会自动续期
public void doSendEmail() {
//创建分布式锁
RLock lock = redisson.getLock("lock_grant_expire_notice");
try {
//获取分布式锁(参数1:等待时间,参数2:过期时间 参数3:时间单位)
if (lock.tryLock(0, 120000, TimeUnit.MILLISECONDS)) {
//抢到锁把计数归零
RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
//获取邮件是否已发送标识
String isSendFlag = redisClusterConfig.get("mail_is_send_flag");
//未发送
if (StringUtils.isBlank(isSendFlag)) {
//发送邮件的真正业务
//sendMail();
redisClusterConfig.setEx("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60);
} else {
//已发送
log.info("mail already send,there's no need to send it twice");
}
} else {
//未获取到分布式锁
Thread.sleep(10000);
if (RECURSIVE_CALL_TIMES <= 5) {
RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
doSendEmail();
}
}
} catch (Exception e) {
log.error("execute send mail fail,message:" + e);
}finally{
//释放分布式锁
lock.unlock();
}
}
4.本地缓存Caffeine
Caffeine基于LRU算法实现,支持多种缓存过期策略。本地缓存且能设置过期时间。Caffeine 2.X版本基于jdk1.8,如果使用Caffeine 3.X版本则需要使用jdk 11版本
3.1 引包
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
3.2 创建缓存
// 初始化缓存,设置了 1 分钟的写过期,100 的缓存最大个数
Cache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
3.3 使用
int key = 1;
// 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
System.out.println("不存在值,返回null:" + cache.getIfPresent(key));
// 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。
// 如果缓存中不存在该 key 则该函数将用于提供默认值,该值在计算后插入缓存中:
System.out.println("返回默认值:" + cache.get(key, a -> 2));
// 校验 key 对应的 value 是否插入缓存中
System.out.println("返回key对应的value:" + cache.getIfPresent(key));
// 手动 put 数据填充缓存中
int value = 2;
cache.put(key, value);
// 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
System.out.println("返回key对应的value:" + cache.getIfPresent(key));
// 移除数据,让数据失效
cache.invalidate(key);
System.out.println("返回key对应的value:" + cache.getIfPresent(key));
5.死锁
特点:
1.互斥:
一个共享资源同一时间只能被一个线程占用;
2.占有且等待:
线程1已经获得共享资源X,在等待获取共享资源Y的时候,它不会释放已经占有的共享资源X;
3.不可抢占
:线程不能抢占其它线程已经占有的共享资源;
4.循环等待:
线程1等待线程2占有的资源,线程2等待线程1占有的资源,这就是循环等待。
解决死锁:
破坏占有且等待条件
破坏不可抢占条件 ReentrantLock的unlock()方法来释放自己持有的锁
破坏循环等待条件
定时检测死锁 jstack 工具是一个线程堆栈分析工具
6.缓存数据一致性问题
因为数据库与缓存是不同的组件,操作必须有先后顺序,无法像数据库的事务一样满足ACID的特性,所以就会出现数据在缓存中与在数据库中不一致的问题
解决方案(6种):
①先更新db,再删除缓存:
如果更新db成功,删缓存失败,将导致数据不一致
②先更新db,再更新缓存:
并发更新场景下,更新缓存会导致数据不一致
③先更新缓存,再更新db:
并发更新场景下,更新缓存会导致数据不一致
④ 先删缓存,再更新db:
请求 A删除缓存,这时请求B来查,就会击穿到数据库,B读取到旧的值后写入缓存,A正常更新db,由于时间差导致数据不一致的情况
⑤ 缓存延时双删:
(1)先淘汰缓存
(2)再写数据库
(3)休眠N秒,再次淘汰缓存。这里是N是大于更新DB后的写缓存时间。即,更新DB后第一次写入redis缓存,再删除redis
sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可
⑥变种双删,前置缓存过期时间
7.redis持久化的两种方式
1 RDB
开启
save 900 1 #900 秒之内,对数据库进行了至少 1 次修改;
save 300 10 #300 秒之内,对数据库进行了至少 10 次修改
save 60 10000 #60 秒之内,对数据库进行了至少 10000 次修改
save :
同步保存操作,会阻塞 Redis 主线程;
bgsave :
fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项
定义:
RDB是在指定时间间隔内将内存中的数据集快照写入磁盘,也就是snapshot快照,它恢复时是将快照文件直接读到内存里
备份执行:
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束,再用这个临时文件
替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作,这确保了极高的性能如果需要进行大规模数据恢复,且对于数据恢复的完整性不是特别敏感RDB比AO更加高效。
在redis.conf中,可以修改rdb备份文件的名称,默认为dump.rdb
特点:
优点:节省磁盘空间、恢复速度快
缺点:
①虽然redis在fork时使用了写时拷贝技术,但是如果数据庞大,还是比较消耗性能
加粗样式②RDB的缺点是最后一次持久化的数据可能丢失
2 AOF
开启:
redis.conf 中将 appendonly no,修改为 appendonly yes 来开启AOF
定义:
以日志的形式来记录每个写操作,将redis执行过程的所有指令记录下来(读操作不记录),只需追加文件但不可以改写文件,redis启动之初会读取改文件重新构建数据。也就是redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
同步频率设置
appendfsync always:每次写入立即同步始终同步,每次redis的写入都会立刻记入日志;性能较差但数据完整性比较好。
appendfsync everysec:每秒同步每秒同步,每秒记录日志一次,如果宕机,本秒数据可能丢失;更新的命令会放在内存中AOF缓冲区,
每秒将缓冲区的命令追加到AOF文件
appendfsync no:不主动同步.redis不主动进行同步,把同步交给操作系统
特点:
优点: 备份机制更稳健,丢失数据概率更低 可读的日志文本,通过操作AOF文件,可以处理误操作
缺点: 比起RDB占用更多的磁盘空间 恢复备份速度慢 每次读写都同步,有一定的性能压力 存在个别的bug,造成恢复不能
3 Redis 混合持久化
什么是混合持久化?
Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 RDB来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很 长的时间。 Redis 4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化。 通过如下配置可以开启混合持久化(必须先开启aof):
aof‐use‐rdb‐preamble yes
如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。
混合持久化AOF文件结构
优劣势对比
优势 | 劣势 | |
---|---|---|
RDB | 1、适合大规模数据恢复/2、对数据完整性和一致性要求不高更适合使用 3、节省磁盘空间 4、恢复速度快 | 1、Fork的时候,内存中的数据会被克隆一份,大致2倍的膨胀,需要考虑 2、虽然Redis在fork的时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能 3、在备份周期在一定间隔时间做一次备份,所以如果Redis意外down的话,就会丢失最后一次快照后所有修改 |
AOF | 1、备份机制更稳健,丢失数据概率更低 2、可读的日志文本,通过操作AOF文件,可以处理误操作 | 1、比RDB占用更多的磁盘空间 2、恢复备份速度要慢 3、每次读写都同步的话,有一定的性能压力 4、存在个别bug,造成不能恢复 |
8.redis数据淘汰策略(内存不足时自动淘汰)
1.noeviction(默认策略):
不会删除任何数据,拒绝所有写入操作并返回客户端错误消息(error)OOM command not allowed when used memory,此时 Redis 只响应删和读操作;
2.allkeys-lru:
从所有 key 中使用 LRU 算法进行淘汰(LRU 算法:最近最少使用算法);
3.allkeys-lfu:
从所有 key 中使用 LFU 算法进行淘汰(LFU 算法:最不常用算法,根据使用频率计算,4.0 版本新增);
4.volatile-lru:
从设置了过期时间的 key 中使用 LRU 算法进行淘汰;
5.volatile-lfu:
从设置了过期时间的 key 中使用 LFU 算法进行淘汰;
6.allkeys-random:
从所有 key 中随机淘汰数据;
7.volatile-random:
从设置了过期时间的 key 中随机淘汰数据;
8.volatile-ttl:
在设置了过期时间的key中,淘汰过期时间剩余最短的。
LRU: Least Recently Used 最近时间最少使用
LFU: Least Frequently Used 最少频率使用
实际工作中配置:
allkeys-lru
配置方法,在redis.conf中配置maxmemory-policy allkeys-lru
如果一个键是过期了的,那它到了过期时间是不是马上就从内存中被删除了呢?
3种过期数据删除策略
不是。
1.定时删除
立即删除过期的数据
优点:
能保证内存中的数据的最大新鲜度
缺点:
对CPU不好,用处理器性能换区存储空间,redis不可能时时刻刻遍历所有的key,太耗cpu
2.惰性删除
数据到达刚过期时间不做处理,等下次访问数据的时候,如果未过期返回数据,已过期则删除
优点:
对cpu友好
缺点:
对内存不友好。如果一个过期数据永远没被访问那它就永远留在内存空间
3.定期删除
每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
优点:
CPU性能占用设置有峰值,监测频率自定义,内存压力也不会很大
缺点:
定期随机抽取进行检查,可能导致很多key到了时间而没有被删除
在 redis.conf 配置
hz 10
Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描
9.redis底层数据结构之ZSET
sorted set,有序的 set,每个元素有个 score。 score 相同时,按照 key 的 ASCII 码排序。
9.1 ZSet实现原理
情况一 同时满足以下条件时使用 ziplist
编码:
①元素数量小于 128 个
②所有 member 的长度都小于 64 字节
可以自己修改压缩链表的值。
对应 redis.conf 参数:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
情况二 超过阈值之后,使用 skiplist+dict
存储(跳表)
跳表:
我们每相邻两个节点增加一个指针(或者理解为有三个元素进入了第二层),让指针指向下下个节点,这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半
**应用场景:**排行榜
例如:
id 为 6001 的新闻点击数加 1:zincrby hotNews:20190926 1 n6001
获取今天点击最多的 15条:zrevrange hotNews:20190926 0 15 withscores
为什么使用跳表?
跳表的时间复杂度是
O(logn)
,跳表跳表的空间复杂度是O(n)
而链表的随机访问时间复杂度是
ArrayList — O(1)
LinkedList ---- O(n)
9.2 String类型的底层数据结构
底层结构
在 Redis 中,String 数据结构的底层实现方式是 SDS(Simple Dynamic String)。SDS 是为了替代 C
语言中的 char* 类型而设计的一种字符串类型,它可以自动扩展空间、减少内存分配次数,并提供了一些常用操作函数。
SDS 的设计目标是
SDS 的设计目标是:提供更安全、更高效、更易用的字符串类型。相比于传统的 char* 类型,SDS 具有以下优点:
自动扩展空间:SDS 可以根据需要自动扩展空间,而不需要手动指定长度,避免了字符串长度计算错误和内存泄漏等问题。 减少内存分配次数:SDS使用预分配和惰性空间释放等技术,减少了内存分配和释放操作的次数,提高了程序性能。 安全性:SDS
对字符串长度进行了边界检查,防止发生缓冲区溢出等安全问题。 可读性:SDS 的代码易于理解和维护,并且提供了一些方便的操作函数,如字符串长度获取、字符串拼接等
SDS 结构体
buf:用于存储实际的字符串内容。
len:表示字符串长度。
free:表示 buf 属性中未使用的字节数
struct sdshdr {
int len; // 字符串长度
int free; // 空闲空间长度
char buf[]; // 字符数组
};
len表示字符串的长度,free表示SDS中未使用的空间的长度,buf是一个柔性数组,用于存储字符串内容。
SDS的底层实现使用了长度优化技术,即在某些操作中,SDS会主动分配更多的内存,以避免频繁地分配和释放内存带来的性能损耗
9.3 List:双向链表、压缩列表
Redis使用的是双向无环链表,并且具有以下几个特点:
(1)双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
(2)无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
(3)带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
(4)多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
9.4 Hash:压缩列表、哈希表
hash(字典)的实现与Java中的HashMap(JDK1.7)的结构也是一致的,它的数据结构也是数组+链表组成的二维结构,节点元素散列在数组上,如果发生hash碰撞则使用链表串联在数组节点上
9.5 Set:哈希表、整数数组
Redis的set(集合)相当于Java语言里的HashSet,它内部的键值对是无序的、唯一的。它的内部实现了一个所有value为null的特殊字典。
集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。
10.redis速度快的原因
1.内存操作
2.单线程
3.epoll的IO多路复用
4.数据结构的优化
例如:自动扩展空间、减少内存分配次数
11. redis删除一个大key的方法?
大key
(bigkey)是指 key 的 value 是个庞然大物,例如 Hashes, Sorted Sets, Lists, Sets,日积月累之后,会变得非常大,可能几十上百MB,甚至到GB
删除大key的后果
大key直接使用 del 命令进行删除,会导致长时间阻塞,甚至崩溃。 因为 del 命令在删除集合类型数据时,时间复杂度为 O(M),M是集合中元素的个数。 Redis 是单线程的,单个命令执行时间过长就会阻塞其他命令,容易引起雪崩
解决办法:
1.渐进式删除
分批删除,通过 scan 命令遍历大key,每次取得少部分元素,对其删除,然后再获取和删除下一批元素。
2.UNLINK
4.0版本以后可以使用 UNLINK 命令,后台线程释放空间
(1)在所有命名空间中把 key 删掉,立即返回,不阻塞。
(2)后台线程执行真正的释放空间的操作。