写在最前,本人也只是个大三的学生,如果你发现任何我写的不对的,请在评论中指出。
最近实习找工作,碰到一家互联网公司对ES、Redis比较执着(我就老老实实的准备了基础),鉴于我实在太菜了,现在赶紧补一补。
Redis简介
以目前的互联网网站的发展,传统的关系型数据在应付动态网站,特别是像淘宝、当当、头条之类的超大规模和高并发的纯动态网站已经显得力不从心了,这里面存在着很多问题:比如商城网站中对商品数据频繁查询、对热搜商品的排行统计、订单超时、微信朋友圈音频视频的存储等。
Redis可以被用于解决以上的问题,它是基于内存数据存储的数据库,高性能,支持丰富的数据类型,并且还支持持久化。
数据类型
- String类型
它的内存存储模型说白了就是键值对,这并没什么好说的,需要提一下的是,Redis在缓存数据序列化的时候,如果你使用的是默认的数据库存在策略,你在Redis可视化工具看到会是一串\x82\D2
类似这样的字符串,所以我推荐的一种RedisSerializable策略你可以参考一下(很方便,并且取出后可以直接转换为相应对象):
// 首先需要导包
<!--json工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
@SuppressWarnings("unused")
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static{
// 解禁autotype
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJson2JsonRedisSerializer(Class<T> clazz){
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException{
if (t == null){
return new byte[0];
}
// 使用WriteClassName的特性支持自省功能优化,带有@type属性的文本将会被自动识别类型
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException{
if (bytes == null || bytes.length <= 0){
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper){
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class<?> clazz){
return TypeFactory.defaultInstance().constructType(clazz);
}
}
// 在redisConfig中配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
redisTemplate.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
但这个可能会有坑!!自增一个key的值,设置时incr 一个数字,但是get时? 是什么类型呢? 其实是动态的可能是 Integer 可能是Long
- List类型
内存存储模型如下:
介绍:
常用命令也就是lpush,rpush,lpop,rpop,lrange等
,也就是可以左插入右插入,左弹出右弹出而已。List的应用场景特别多,比如twitter的关注列表,粉丝列表都可以用list结构来实现。list实际上就是链表,使用List结构我们可以轻松地实现最新消息排行等功能。List的另一个应用就是消息队列,利用PUSH操作,将任何存到list中,然后工作线程再POP操作将任务取出。
- Set/ZSet类型
两者内存模型:
介绍:
两者都是不可重复,区别是Sorted Set可以通过用户额外提供的一个优先级的参数来为成员排序,并且是插入有序的,即自动排序。Set集合的概念就是一堆不重复值的组合,利用它可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,或者将其所有的粉丝存在一个集合里。而Sorted Set可以用来做带权重的队列,比如普通消息的score为1, 重要消息的score为2,然后工作线程格局score的倒叙来获取工作任务。
实现方式:
跟java中的HashSet类似,set的内部实现是一个value值为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能判断一个成员是否在集合内的原因。
- hash类型
内部存储结构:
介绍:
简单描述一下hash应用的场景,比如我们要存储一个用户信息对象数据,包含以下信息:用户ID为查找的key, 存储的value为用户对象包括姓名、年龄、生日等信息
,如果用普通的key/value来存储,则:
第一种方式:用户ID为查询key, 其他信息封装成一个对象以序列化方式存储, 这种方式的缺点就是增加序列化与反序列化的开销,修改某项时,需要把整个对象取出,并且修改需要对并发进行保护。
第二种方式:用户信息对象有多少属性就存多少个key-value对, 用用户ID+对应属性的名称作为唯一标识来取得对应数据,虽然节省了序列化与反序列化开销和并发问题,但内存的浪费还是非常可观的。
两种方法都不太可行, 所以redis有了hash类型, 你观察内存结构图就知道了: hash实际是内存存储的value为一个hashmap。也就是说,key仍然可以是用户ID, value则是一个map, 这样对数据的修改和存户都可以直接通过其内部的map的key(Redis里称内部的map的key为field),也就是可以通过key(用户ID)+ field(属性标签)就可以操作对应属性数据,这样就解决了以上两个问题了。
持久化机制
这个在面试时被问到了,当时只是讲了一下模糊的概念,只不过HR没有深追我问题,现在记录一下。
Redis官方提供了两种不同的持久化方法来将数据存储到硬盘里:分别是快照和AOF只追加日志文件。
快照(Snapshot)
特点: 这种方式就是将某一时刻的所有数据都写入硬盘中,当然这也是redis的默认开启持久化方式,保存的文件是.rdb形式结尾的文件,因此这种方式也被称为RDB方式。
生成方式: BGSAVE指令和SAVE指令
- 1、 客户端方式之BGSAVE
你可以在linux的客户端中使用BGSAVE命令来创建一个快照,当接收到客户端的BGSAVE命令时, redis就会用fork来创建一个子进程,然后子进程负责将快照希写入磁盘中,而父进程则继续处理命令请求。
这里需要注意的是创建父子进程是为了不使创建快照的过程中阻塞当前父进程的动作。 fork是一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会被优化:在刚开始的时候,父子进程会共享同内存,直到父进程或子进程对内存进行了写操作,共享服务才会结束。
- 2、客户端方式之SAVE
这个也是用于创建快照的,只是它会阻塞当前redis进程,在快照创建完毕之前将不再响应任何其他命令。
注意,这个命令并不常用,也不推荐使用, 并且如果你在客户端使用shutdown
命令结束当前redis进程,也是会调用SAVE命令的。 并且如果你想作死, 在关闭redis服务后, 你可以找到.rdb文件后缀的文件,去删除它,嘿嘿。
AOF只追加日志文件
现在大部分都是采用这种方式的,这种方式可以将所有的客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF文件末尾, 以此来记录数据发送的变化,因此只要redis从头到尾执行一遍AOF文件所包含的所有写命令,就可以恢复数据集了。
1、开始AOF持久化
# 在redis的默认配置中AOF是没有开启,需要在配置文件中开启
# 开始AOF持久化
- a. 修改 appendonly yes 开始持久化
- b. 修改 appendfilename "appendonly.aof" 指定生成名
- c. 修改appendfsync everysec|always|no 指定
2、日志追加频率
- always: 不推荐。 每个redis写入命令都要同步写入硬盘,这会严重降低redis速度的。 如果用户使用了这个选项,确实能够将系统奔溃时出现的数据丢失损失降到最低(极端情况甚至不会出现丢失)。但这种策略需要对硬盘进行大量的写入操作,要知道**普通的转盘式硬盘每秒大概200+左右的命令; 固态硬盘是几百万的命令。**但如果公司穷,还是不推荐。
- everysec:推荐。 每秒一次的频率对AOF进行同步;这个很好理解, redis可以保证,即使系统奔溃, 用户最多丢失一秒之内产生的数据。
- no:不推荐。 由操作系统决定何时同步。这个会丢失不定量的数据,而且是转盘式硬盘的话写入操作不够快,当缓冲区被等待写入硬盘数据填满时,会导致redis进入阻塞状态。
3、AOF文件的重写
AOF的方式带来一个问题,不仅是文件的越来越大,如果我们调用incr test
命令一百次, 文件中存在100次的命令但有99次是不必要的。 因此, redis提供了重写机制:
# 1.客户端方式触发重写
- 执行BGREWRITEAOF命令 不会阻塞redis的服务
# 2.服务器配置方式自动触发
- 配置redis.conf中的auto-aof-rewrite-percentage选项
- 如果设置auto-aof-rewrite-percentage值为100和auto-aof-rewrite-min-size 64mb,并且启用的AOF持久化时,那么当AOF文件体积大于64M,并且AOF文件的体积比上一次重写之后体积大了至少一倍(100%)时,会自动触发,如果重写过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大
4、重写原理:
重写AOF文件,并没有读取旧文件,而是将整个内存中的数据库内容用命令重写了一个新的AOF文件:
- 1、redis调用fork, 现在有父子进程两个, 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令。
- 2、父进程继续处理客户端的请求,除了把写命令写入到原来的aof文件中。同时把收到写命令缓存起来,这样就能保证如果子进程重写失败的不会出现问题。
- 3、当子进程把快照内容写入已命令方式写到临时文件中后,子进程发出消息通知父进程,然后父进程把缓存的写命令也写入到临时文件。
- 4、现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof中追加。
简单的实现分布式缓存
在使用mybatis的时候,大家肯定有这样一个技巧, 那就是在开启二级缓存,但这样是存在一些缺陷的,因为开启二级缓存后只是缓存于当前应用服务器中, 如果只是本地单机运行应用服务器肯定是没有问题,但是要运行在多台服务器中肯定行不通。所以,今天就来解决一下这个问题:
// 首先解决一下导包问题
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
// 按例配置好mysql、druid与myabtis之后, 可以设置一下dao层的log等级,如下:
logging.level.com.example.demo.dao=debug
接下来需要做的是,简单的实现一个Object类(与你的数据库某一张表对应,数据随便,能用就行)、Service层和Dao层(实现一个简单findAll方法)
开启mybatis二级缓存的方式就是在*mapper.xml
文件中追加<cache/>
标签,实际上这个标签可以添加type属性,这里这个type属性默认是由PerpetualCache
类实现(类很短, 可以看一看), 该类只是实现了Cache接口,我们也可以使用Redis实现该接口,并接替它的工作实现分布式缓存,也就是说:
public class RedisCache implements Cache{
// 模仿 但必须有个构造函数 mapper的namespace作为缓存唯一id
private final String id;
public RedisCache(String id) {
this.id = id;
}
// 返回cache的唯一标识
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
System.out.println("key = " + key.toString());
System.out.println("value = " + value);
// 使用redis中的hash类型作为缓存类型
opsRedisTemplate().opsForHash().put(id, key.toString(), value);
}
@Override
public Object getObject(Object key) {
System.out.println("key = " + key.toString());
return opsRedisTemplate().opsForHash().get(id, key.toString());
}
// 根据指定的key删除缓存 保留方法 日后可能会实现
@Override
public Object removeObject(Object key) {
return null;
}
// 清空缓存 执行任何增删改都会执行这个方法
@Override
public void clear() {
opsRedisTemplate().opsForHash().delete(id);
}
@Override
public int getSize() {
return opsRedisTemplate().opsForHash().size(id).intValue();
}
private RedisTemplate opsRedisTemplate(){
RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
最好复制我这段代码去做做实验,观察一下key与field的值。另外,开启<cache/>
在进行增删改时,只会清空当前执行这条语句的缓存,这如果我们进行多表操作时是很不利的。所以推荐使用<cache-ref namespace=""/>
在主类中添加引用类的namesapce(namespace指的是你的dao接口的reference)。