如果没有用docker,直接从官网下载然后丢服务器就行了,如果用的是docker那么就麻烦一点,这里我整了一个脚本来一键安装/启动:
#!/bin/bash
workdir="/opt/arthas-dev"
user=user
container=$1
if [ -z $container ];then
echo -e "\e[32m用法: arthas-launcher.sh [容器名|容器ID] [用户名(可选,必须和启动应用的用户一致)]\e[0m"
exit 0
fi
if [ ! -z $2 ];then
user=$2
fi
function installArthas() {
echo "开始安装arthas..."
docker exec -u $user $container mkdir $workdir/arthas
docker cp jdk-8u381-linux-x64.tar.gz $container:$workdir/jdk.tar.gz
docker cp arthas-packaging-3.7.1-bin.zip $container:$workdir/arthas/arthas.zip
docker exec -u root $container chmod 777 $workdir/jdk.tar.gz
docker exec -u root $container chmod 777 $workdir/arthas/arthas.zip
docker exec -u $user $container tar zxvf $workdir/jdk.tar.gz -C $workdir
docker exec -u $user $container unzip $workdir/arthas/arthas.zip -d $workdir/arthas
docker exec -u $user $container touch $workdir/installedMark
echo "安装成功!"
}
# --------------main--------------
docker exec -u $user $container test -d $workdir
if [ ! $? -eq 0 ];then
docker exec -u root $container mkdir $workdir
docker exec -u root $container chown $user $workdir
installArthas
fi
docker exec -u $user $container test -e $workdir/installedMark
if [ $? -eq 0 ];then
docker exec -it -u $user $container $workdir/jdk1.8.0_381/bin/java -jar $workdir/arthas/arthas-boot.jar
else
echo "文件完整性校验失败! 重新尝试安装arthas."
installArthas
fi
bash
例如有一个Encoder的encrypt方法我们想要观察,但是由于每次请求这个方法都会被调用很多次,如果不加限制,每次会爆出很多不相干的信息。
watch提供了一个condition-express选项来帮助我们过滤输出:
watch xxx.Encoder encrypt {returnObj} 'params[0]=="P@ssw0rd"'
bash
上面这条指令则是让arthas在第一个参数是P@ssw0rd
的时候输出调用的返回值
有些时候,代码抛出了异常,但是又被另外一个异常包了一层抛出去了,例如throw new RuntimeException(e)
,甚至有的时候
没有被抛出:log.error(e.getMessage())
,导致我们不能看到我们想要的调用栈。
而watch也提供了观察异常抛出的功能。假如有一个Encoder的encrypt方法抛出了一个ExceptionA,但是没有打印栈信息,我们可以用 如下指令:
watch xxx.Encoder encrypt {throwExp} -e -x 2bash
-e
表示抛出异常才触发。
-x
表示输出属性的遍历深度,这个是啥意思呢,例如-x的值为1的时候,watch输出的结果可能就只是对象的toString,而-x的值为2的时候,watch也会输出这个对象里面属性的toString,如果等于3,则是对象里面的属性的属性的toString...
随缘更新。。
通过AnnotationConfigApplicationContext
可以创建一个Spring容器:
public class MySpringApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) applicationContext.getBean("userService");
userService.test();
}
}
@ComponentScan("pers.xds.springboot")
public class AppConfig {
}
java
可以在AbstractAutowireCapableBeanFactory#doCreateBean
中看到完整的bean生成流程。
大致分为如下过程:
1.对Bean进行实例化
2.依赖注入
3.如果Bean实现了BeanNameAware
接口,Spring将调用setBeanName()
,设置 Bean
的 id(xml文件中bean标签的id)
4.如果Bean实现了BeanFactoryAware
接口,Spring将调用setBeanFactory()
5.如果Bean实现了ApplicationContextAware
接口,Spring容器将调用setApplicationContext()
6.如果存在BeanPostProcessor
,Spring将调用它们的postProcessBeforeInitialization
(预初始化)方法,在Bean初始化前对其进行处理
7.如果Bean实现了InitializingBean
接口,Spring将调用它的afterPropertiesSet
方法,然后调用xml定义的 init-method方法(初始化),两个方法作用类似,都是在初始化 bean 的时候执行
8.如果存在BeanPostProcessor
,Spring将调用它们的postProcessAfterInitialization
(后初始化)方法,在Bean初始化后对其进行处理
9.Bean初始化完成,供应用使用,直到应用被销毁
10.如果Bean实现了DisposableBean
接口,Spring将调用它的destory
方法,然后调用在xml中定义的 destory-method
方法,这两个方法作用类似,都是在Bean实例销毁前执行。
BeanFactory
:管理Bean的容器,Spring中生成的Bean都是由这个接口的实现来管理的。
FactoryBean
:让开发者以编程的方式来创建一个bean,一般用于创建比较复杂的bean。
1、使用@Configuration
与@Bean
注解
2、使用@Controller
、@Service
、@Repository
、@Component
注解标注该类,然后启用@ComponentScan
自动扫描
3、使用@Import
方法,使用@Import注解把bean导入到当前容器中。
1、singleton:单例,Spring中的bean默认都是单例的。
2、prototype:每次请求都会创建一个新的bean实例。
3、request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
4、session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
5、application:限定一个Bean的作用域为ServletContext
的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。
@Autowired
注解会优先根据类型来注入,当有多个bean时,会尝试根据变量名来注入(byname),如果没有找到就抛出异常。
可以通过@Qualifier
来指定要注入的bean的名称。
@Resource
注解会优先byname,找不到再byType。
@Bean
只能作用于方法上,表示这个方法会返回一个Bean,一般需要配合@Configuration
使用。
@Component
只能作用于类型上,表示这个类会作为组件类,并告诉Spring要为这个类创建bean。
Spring: @Bean can still work without @Configuration - Stack Overflow
@Bean
在@Configuration
表示的类里使用时,Spring会为其自动创建一个动态代理对象,在同一个配置类中可以直接调用方法来获取Bean:
@Configuration
public class ExampleConfiguration {
@Bean
public Datasource datasource() {
BasicDatasource datasource = new BasicDatasource();
// ...
return datasource;
}
public PlatformTransactionManager transactionManager() {
// 注意这里是直接调用了方法,每次调用都会返回同一个bean,并不会多次创建
return new DataSourceTransactionManager(datasource());
}
}
java
而在非@Configuration
下定义的@Bean
会以Lite Mode运作,在该模式下调用其它@Bean
方法时,则是普通的方法调用(没有代理对象去拦截调用)。
对于构造器注入的循环依赖:Spring处理不了,直接抛出BeanCurrentlylnCreationException
异常。
非单例循环依赖:无法处理。
单例模式下属性注入的循环依赖会通过三级缓存处理循环依赖:
singletonObjects
:完成了初始化的单例对象map,bean name --> bean instance
earlySingletonObjects
:完成实例化未初始化的单例对象map,bean name --> bean instance
singletonFactories
: 单例对象工厂map,bean name --> ObjectFactory,存放 bean 工厂对象
具体的执行逻辑可以在DefaultSingletonBeanRegistry
中看到,这里贴出核心方法:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized(this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
java
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑,如果业务逻辑有对单例状态的修改(体现为此单例的成员属性),则必须考虑线程安全问题。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,那么不会有线程安全问题;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
无状态bean和有状态bean
在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能。有状态的Bean在多线程环境下不安全,一般用Prototype模式或者使用ThreadLocal解决线程安全问题。
阿里面试真题:Spring容器启动流程_spring启动流程面试题_敖 丙的博客-CSDN博客
常见的动态代理有两种:
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才生成代理对象。
CGLIB动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy的优势:
最小化依赖关系、代码实现简单、简化开发和维护、JDK原生支持,比CGLIB更加可靠,随JDK版本平滑升级。而字节码类库通常需要进行更新以保证在新版Java上能够使用。
CGLIB的优势:
无需实现接口,达到代理类无侵入,只操作关心的类,而不必为其他相关类增加工作量。高性能。
Java动态代理之一CGLIB详解 - 知乎 (zhihu.com)
可能是最漂亮的Spring事务管理详解 - 掘金 (juejin.cn)
一文搞懂Mybatis架构与工作原理 - 掘金 (juejin.cn)
加载映射文件(通过动态代理和xml为接口生成对应的代理类)
构造会话工程(SqlSessionFactory)
创建会话对象(SqlSession)
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
java
Executor执行器
Mybatis会通过Executor去执行SQL语句。一般这里面会有缓存的实现。
MappedStatement对象
对映射信息的封装,它存储了一个SQL对应的所有信息。Mybatis 通过解析 XML 和 mapper 接口上的注解,生成 sql 对应的 MappedStatement 实例
输入参数映射
输出参数映射
将数据库输出转换为 Map
,List
或自定义的类型
深入理解Redis 数据结构—字典 - 知乎 (zhihu.com)
可以这样理解:Redis的字典就是java7的HashMap,即哈希表+链表
Redis字典使用的哈希表结构如下:redis/dict.h at 2.6 · redis/redis (github.com)
typedef struct dictht {
// table 数组
dictEntry **table;
// 哈希表的大小
unsigned long size;
// 等于size-1,用于计算索引值, 这里说明size肯定是2的幂
unsigned long sizemask;
// 已有的键值对数量
unsigned long used;
} dictht;
c++
dictEntry
就是哈希节点了:redis/dict.h at 2.6 · redis/redis (github.com)
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
c++
Redis中的字典则由dict
组成:redis/dict.h at 2.6 · redis/redis (github.com)
typedef struct dict {
// 类型特定的函数,提供增删改查等功能
dictType *type;
// 私有函数
void *privdata;
// 哈希表, 这里的二维是后面用来扩容的
dictht ht[2];
// rehash 索引,记录了当前扩容的进度
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 用来记录当前运行的安全迭代器数,当不为0的时候表示有安全迭代器正在执行,这时候就会暂停rehash操作
int iterators; /* number of iterators currently running */
} dict;
c++
我们用ht[0].used/ht[0].size
表示负载因子
如果没有fork子进程在执行RDB或者AOF的持久化,一旦满足负载因子大于等于1,此时触发扩容;
如果有fork子进程在执行RDB或者AOF的持久化时,则需要满足负载因子大于5,此时触发扩容。
Redis在扩容时使用的是渐进式哈希,即每次值移动一部分的数据到新的哈希表中。
在字典dict
中,dict.ht[0]
代表旧的哈希表,dict.ht[1]
代表新的哈希表,每次扩容时会将容量乘2,同时dict.rehashidx
代表rehash的进度,表示dict.ht[0]
中,小于该索引的值都已经被移动到dict.ht[1]
中了,此时需要在dict.ht[1]
中进行相关的增删改查操作,反之则在dict.ht[0]
中进行。
在扩容期间,每次进行增删改查都会将dict.rehashidx
加一,并进行相关的rehash操作。
在扩容完毕后,将dict.ht[0]
指向dict.ht[1]
,并删除旧的哈希表。
当负载因子小于0.1时,Redis就会对哈希表进行收缩操作。
相关操作和扩容一样,在dict.ht[1]
处创建新的哈希表,之后再渐进式rehash。
假如在rehash扩容的时候,我们一直插入,会不会导致再次扩容呢?
假设此时哈希表容量为n
,元素数量为n
,在扩容哈希表容量后变为2n
,而对于Redis来说,完成rehash需要2n - n = n
次操作,所以我们最多进行n
次插入,插入完后元素数量也变为2n
,再次触发扩容。
对于负载因子为5的时候,假设此时哈希表容量为n
,元素数量为5n + 1
,扩容后哈希表容量为2n
,同样我们可以插入n
个元素,此时元素数量变为6n + 1
,负载因子为(6n + 1) / 2n
约等于3,此时不会触发扩容。
使用如下指令就会执行全遍历,返回所有的key:
keys *shell
优点:
缺点:
迭代器结构:
typedef struct dictIterator {
dict *d; //迭代的字典
int index; //当前迭代到Hash表中哪个索引值
int table, safe; //table用于表示当前正在迭代的Hash表,即ht[0]与ht[1],safe用于表示当前创建的是否为安全迭代器
dictEntry *entry, *nextEntry;//当前节点,下一个节点
/* unsafe iterator fingerprint for misuse detection. */
long long fingerprint;//字典的指纹,当字典未发生改变时,该值不变,发生改变时则值也随着改变
} dictIterator;
c++
Redis源码学习——安全迭代器和非安全迭代器(一)_damanchen的博客-CSDN博客
Redis源码学习——安全迭代器和非安全迭代器(二)_damanchen的博客-CSDN博客
跋山涉水 —— 深入 Redis 字典遍历 - 知乎 (zhihu.com)
安全迭代器:
非安全迭代器:
fingerprint
字段来判断字典是否发生变动使用scan
命令可以间接遍历,这个命令每次会返回一个下一个需要遍历的索引值:
redis 127.0.0.1:6379> scan 0 1) "17" 2) 1) "key:12" 2) "key:8" 3) "key:4" 4) "key:14" 5) "key:16" 6) "key:17" 7) "key:15" 8) "key:10" 9) "key:3" 10) "key:7" 11) "key:1" redis 127.0.0.1:6379> scan 17 1) "0" 2) 1) "key:5" 2) "key:18" 3) "key:0" 4) "key:2" 5) "key:19" 6) "key:13" 7) "key:6" 8) "key:9" 9) "key:11"shell
优点:
缺点:
redis scan 命令底层原理(为什么会重复扫描?)_redis scan命令原理_柏油的博客-CSDN博客
这里第一次看可能会有这个疑问,我们打个比方:
遍历顺序:00 -> 10 -> 01 -> 11
若正好遍历到10时扩容完毕了,则新顺序为:
000 -> 100 -> 010 -> 110 -> 001 -> 101 -> 011 -> 111
此时我们在第三个位置,即010那里。
这时候可能就有疑问了:100那里不就遍历不到了吗?这不是丢数据了吗?
但这样其实是想多了,我们来看000和100,假如哈希表长度为4时,这两个索引下的元素会落到哪个哈希表下?
很明显,这两个位置的元素都会落到00这个索引的下面,因为哈希表长度为4时,索引位置的取法就是和0x11
做与操作,而000和100低两位相同,所以它们俩在之前就在00处,以链表的形式组合在了一起,当遍历到10时,100也肯定被遍历了。
总结一下就是scan
命令会在哈希表缩容的时候造成数据重复,在rehash的期间也会造成重复。
在rehash期间调用scan
,Redis会先扫小表,假如最终索引为v,然后会接着在大表中从v开始扫。
在看数据类型前,我们再回顾一下entry的结构:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
c++
有没有发现一个问题:这个v代表值,那么这个值是个什么东西??
这里其实是C语言的union,可以让多个变量使用同一个内存空间:C/C++ union 使用教程 (常见操作与缺陷) - 知乎 (zhihu.com)
你可以这样理解:这里的v即有三种类型,即void*
、uint64_t
(64位无符号整数)、int64_t
(64位有符号整数)。
对应void*
你可以理解为Java中的Object
类型,用它做参数的话就可以传入任意对象,更详细的信息可以看这篇博客:void*(指针)的类型转换-专讲_void*转换_NeverLate_gogogo的博客-CSDN博客
一般情况下void*
都是指向redisObject
:redis/README.md at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
};
c++
type
:没啥好说的,每种数据结构的标识符
encoding
:编码
以string来说,就有三种:int
, embstr
, raw
:
127.0.0.1:6379> SET counter 1 OK 127.0.0.1:6379> OBJECT ENCODING counter "int" 127.0.0.1:6379> SET name "Tom" OK 127.0.0.1:6379> OBJECT ENCODING name "embstr" 127.0.0.1:6379> SETBIT bits 1 1 (integer) 0 127.0.0.1:6379> OBJECT ENCODING bits "raw"shell
lru
:给Redis做内存淘汰用
refcount
:引用计数,这个值为0的时候这个对象会被清除
ptr
:指向对象的实际表示,可能有多个指向同一个对象,一般还要配合encoding判断
Redis 的字符串是动态字符串,是可以修改的字符串,可以勉强理解为Java里的StringBuilder
。
当字符串需要扩容时,有如下两种情况:
字符串最大长度为512MB。 数据结构:redis/sds.h at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
c++
我们可以发现,字符串结构体基本由len
(已使用的长度)、alloc
(最大长度/分配的长度)、flags
(标志信息)、buf
(字符串内容)组成。
字符串拼接:redis/sds.c at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis · GitHub
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
// 内存不足
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
c++
要懂redis,首先得看懂sds(全网最细节的sds讲解) - 知乎 (zhihu.com)
Redis之Hash数据结构底层原理_redis hash原理_不要迷恋发哥的博客-CSDN博客
hashtable就和Redis最外层的字典是差不多的。
对于List同样也有两种编码:
满足如下条件时,压缩列表会被转换为双向链表:
Redis列表list 底层原理 - 知乎 (zhihu.com)
ziplist
的运作方式类似于一个队列:
area |<---- ziplist header ---->|<----------- entries ------------->|<-end->| size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte +---------+--------+-------+--------+--------+--------+--------+-------+ component | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend | +---------+--------+-------+--------+--------+--------+--------+-------+ ^ ^ ^ address | | | ZIPLIST_ENTRY_HEAD | ZIPLIST_ENTRY_END | ZIPLIST_ENTRY_TAILtext
字段 | 类型 | 说明 |
---|---|---|
zlbytes | uint32_t | 整个ziplist占用的内存字节数。 |
zltail | uint32_t | 到达ziplist表尾节点的偏移量。 |
zllen | uint16_t | ziplist中节点的数量。 |
entryX | ? | ziplist的各个节点。 |
zlend | uint8_t | 常量0b111111,用于标记ziplist末尾。 |
每个entry
的结构是这样的:
+-----------------------+----------+---------+ | previous_entry_length | encoding | content | +-----------------------+----------+---------+text
previous_entry_length
:前面一个节点的长度(字节)。若前面一个节点的长度小于254字节,则该属性占1个字节的宽度,反正则是占5个字节的宽度,第一个字节是常量0xFE
(254)
encoding
:记录content的类型
content
:保存节点的值
我们可以发现ziplist
不能从头开始遍历,因为每个节点的长度都是不一样的,在遍历的时候需要根据zltail的值从尾部开始向前遍历。
Set拥有两种编码:
满足如下条件时intset将会被转换成hashtable:
数据结构:
typedef struct intset {
// 这个编码用来决定contents的大小
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
c++
redis/intset.h at 971b177fa338fe06cb67a930c6e54467d29ec44f · redis/redis (github.com)
面试官:说说Redis中Set数据类型的底层实现 - 掘金 (juejin.cn)
zset有两种实现:
满足如下条件时zipList会转换成skipList:
Redis 跳跃表skiplist(深入理解,面试再也不用怕)_redis skiplist_妖四灵.Shuen的博客-CSDN博客
节点数据结构:
typedef struct zskiplistNode {
// 当前保存的值
sds ele;
// 分值,用于排序
double score;
// 前一个节点
struct zskiplistNode *backward;
// 当前层节点
struct zskiplistLevel {
struct zskiplistNode *forward;
// 跳表的跨度
unsigned long span;
} level[];
} zskiplistNode;
c++
redis/server.h at 971b177fa338fe06cb67a930c6e54467d29ec44f · redis/redis (github.com)
// 跳表
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
// 跳表的长度
unsigned long length;
// 最高层数
int level;
} zskiplist;
// zset数据结构
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
c++
RDB即Redis Database,它会将Redis某一时刻的数据以文件的形式全量备份到磁盘。
Redis提供了两个指令来生成RDB,一个是SAVE,这个命令会阻塞主线程,直到RDB生成完毕。
另外一个则是BGSAVE,这时会fork一个子进程去专门负责写入RDB。
在读取数据时用到了写时复制(COW)技术,fork创建出的子进程,与父进程共享内存空间。也就是说,如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!
LInux fork的写时复制(copy on write)_fork写时复制_富士康质检员张全蛋的博客-CSDN博客
AOF即Append Only File,它会记录在redis服务器上执行过的命令来实现持久化的目的。
Redis详解(七)------ AOF 持久化 - YSOcean - 博客园 (cnblogs.com)
Redis默认每秒执行一次AOF的写入。
为了防止AOF文件过大,当AOF的文件大小超过设定的阈值后,Redis就会启动AOF的文件压缩。
压缩前:
sadd animals "cat" sadd animals "dog" sadd animals "cat" sadd animals "lion"shell
压缩后:
sadd animals "cat" "dog" "lion"shell
Redis对于过期的key有两种删除策略:
Redis会将每个设置了过期时间的key放到一个独立的字典中,以后会定期按照某种算法来遍历里面的key。
默认每秒进行10次扫描,每次会随机选取一些key,然后删除其中过期的key,若某次删除的数量超过了选取的1/4
,则会重复这一步骤。
在客户端访问某个key时,若这个key过期,则会将其删除。
当没有可以被删除的key,且Redis内存不足时,此时会根据内存淘汰策略删除一些没有过期的key。
noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
allkeys-random:加入键的时候如果过限,从所有key随机删除
volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu:从所有键中驱逐使用频率最少的键
关于LRU和LFU,在redisObject里会保存相关的参数:
struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
};
c++
redis/README.md at cb1717865804fdb0561e728d2f3a0a1138099d9d · redis/redis (github.com)
lru
整个值都用来表示相关访问时间。LRU即Least Recently Used-最近最少使用算法。
LRU会维护一个双向链表,对于新加入的数据或者最近被访问的数据,它们会被存/移动到链表头部,当内存不足的时候删除链表尾部的数据。
优点:
缺点:
LFU即Least Frequently Used-最不经常使用。其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
Redis的LFU和上面的LFU有一些不一样,一般的LFU有如下缺点:
而Redis在此基础上会将每个数据的访问次数设置为5
,每次访问会根据其上次访问时间扣除一定的访问次数,然后再根据生成的一个随机数,决定是否对访问次数字段加一:
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
// 访问次数越多,加一的概率越小
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
c++
其中server.lfu_log_factor
默认为10。
因为访问计数器的长度为8位,最大为255,如果每次访问都加一,很可能会导致溢出。
16 | LFU算法和其他算法相比有优势吗? (geekbang.org)
一文搞懂 Redis 的三种集群方案 - 腾讯云开发者社区-腾讯云 (tencent.com)
Redis支持三种集群方案:
主从复制主要由一个主数据库与一个或多个从数据库实例组成。
客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。
具体工作机制为:
优点:
缺点:
难以支持在线扩容,Redis的容量受限于单机配置
master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。
哨兵顾名思义,就是来为Redis集群站哨的,一旦发现问题能做出相应的应对处理。其功能包括:
Redis哨兵(Sentinel)模式 - 简书 (jianshu.com)
优点:
缺点:
Cluster采用无中心结构,它的特点如下:
Cluster模式的具体工作机制:
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
八股文|后端|MySQL|答案 - 力扣(LeetCode)
Redis 内存淘汰策略 (史上最全)_redis内存淘汰策略_40岁资深老架构师尼恩的博客-CSDN博客
Redis(主从复制、哨兵模式、集群)的讲解_redis集哨兵模式哪个节点负责读取数据_五条悟的小迷妹的博客-CSDN博客
Redis有哪几种持久化方式?优缺点是什么_Listener_code的博客-CSDN博客