背景:
本文介绍Redis相关知识,包括Redis的使用、单线程机制、事务、内存过期和淘汰机制。后续将在《三方件-3 Redis持久化机制》中介绍Redis基于RDB和AOF的持久化机制;在《三方件-4 Redis集群》介绍主从、哨兵和Cluster集群相关的内容;在《三方件-5 Redis缓存问题》中介绍缓存击穿,缓存穿透,缓存雪崩等问题。
1.Redis介绍
C语言开发的、基于内存的、跨平台的非关系型数据库(Nosql)。Redis 基于键值对存储结果,键只能为字符串,值的类型有:字符串String
、散列 Hash
、列表 List
、集合 Set
、有序集合 Sorted Set
。因其基于内存而脱离了常规数据库的IO操作,从而具备高性能的读写(10W/s 的读和 8W/s 写)。核心处理使用单线程 进行,避免了并发问题,以及减少了线程切换带来的性能开销。
redis作为内存数据库,可用于存放缓存数据。在高并发场景中存放热点数据,可以提高数据访问速度,也可以缓解数据库压力;除此之外,分布式锁也是Redis的一个应用场景。
Memcached 作为Nosql,常用于Redis做比较, 存在以下几个方面的差异:
[1] 效率: Redis 基于单线程(memcached使用多线程),读写小数据时,Redis性能超过Memcached ; 处理较大数据时,Memcached性能超过Redis
[2] 值的类型:redis支持多种数据类型; Memcached 仅支持字符串
[3] 持久化:Redis支持RDB和AOF两种持久化策略,宕机后可恢复数据; Memcached纯内存存储,不支持持久化
[4] 数据同步和分布式:redis基于持久化数据提供了实例之间的数据同步,Redis支持主从复制和数据分区,适用于构建分布式系统 而Memcached不支持
[5] 事务和Lua脚本: Redis支持事务和Lua脚本,可以执行复杂操作; Memcached 不支持
总之,Memcached适用于大量并发读和简单键值存储的场景,而Redis适用于复杂数据结构、数据有持久化或分布式要求的场景等。
2.Java使用方式
可借助docker快速安装redis, 准备redis.conf配置文件:
python
dir ./
dbfilename dump.rdb
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
requirepass xxxxxxx
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
使用docker安装redis环境:
shell
# 准备配置文件和持久化数据存放路径
mkdir -p /path/to/redisdata
touch /path/to/redis.conf
# 启动docker容器
docker run --restart=always
-p 16379:6379
--name myredis
-v /path/to/redis.conf:/etc/redis/redis.conf
-v /path/to/redisdata:/data
-d redis:7.0.12 redis-server /etc/redis/redis.conf
运行后,docker的工作目录为/data,因此可在宿主机的/path/to/redisdata目录下查看持久化数据。
Java中存在以下三种使用Redis方式。
2.1 Jedis
Jedis提供了比较全面的Redis命令(同步的API)支持,本质上是通过socket直连Redis服务器,一个Jedis对象对应一个连接,因此Jedis对象本身是线程不安全的。以下通过案例介绍使用方式。
引入pom依赖:
xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
案例:
java
public class JedisDemo {
public static void main(String[] args) {
// Redis 服务器配置
String host = "127.0.0.1";
int port = 6379;
String password = "123456";
try (Jedis jedis = new Jedis(host, port)) {
// 校验密码
jedis.auth(password);
// 操作1:添加键值对
jedis.set("keyStr", "myStr");
// 操作2:根据键取值
String value = jedis.get("keyStr");
System.out.println("键 keyStr 的值为: " + value);
// 操作3:根据键删除键值对
jedis.del("keyStr");
} catch (Exception e) {
System.out.println(e);
}
}
}
2.2 Redisson
Redission提供了很多分布式功能,如常见的分布式锁。
引入pom依赖:
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.1</version>
</dependency>
案例:
java
public class RedissonDemo {
public static void main(String[] args) throws InterruptedException {
RedissonClient redisson = getRedissonClient();
RLock lock = redisson.getLock("myLock");
// 4. 加锁操作
lock.lock();
try {
// 模拟耗时操作
System.out.println("Begin...");
Thread.sleep(1000 * 10);
System.out.println("End.");
} finally {
// 5. 释放锁
lock.unlock();
}
redisson.shutdown();
}
private static RedissonClient getRedissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
2.3 Lettuce
Lettuce基于Netty框架实现,使用非阻塞IO与Redis服务器通信,是一个高性能的Redis客户端。SpringBoot提供了Starter, 可以在SpringBoot项目中轻松引入。
引入pom依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
在application.yml中添加配置信息:
yml
spring:
redis:
host: 127.0.0.1
port: 6379
#Redis使用的数据库
database: 0
#连接超时事件毫秒
timeout: 18000
lettuce:
pool:
#连接池最大连接数
max-active: 20
#最大阻塞等待时间
max-idle: 5
#连接池最小空闲连接
min-idle: 0
添加配置类:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
案例:
java
@SpringBootTest
public class MyRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testMyRedis() {
String key = "key001";
String value = "value001";
redisTemplate.opsForValue().set(key,value);
assertEquals(value, redisTemplate.opsForValue().get(key));
}
}
上面通过redisTemplate
对象通过不同方法封装了对不同数据类型的处理:
[1] opsForValue()
中封装了操作String
类型的方法;
[2] opsForList()
中封装了操作List
类型的方法;
[3] opsForSet()
中封装了操作Set
类型的方法;
[4] opsForHash()
中封装了操作Hash
类型的方法;
[5] opsForZSet()
中封装了操作ZSet
类型的方法.
3.单线程机制
Redis的核心工作由单线程完成,即使用一个线程完成对内存数据库的读写,由此保证了线程安全性。
虽然在Redis6.0(2020)后引入了多线程机制,但是仅将IO通信部分委托给多线程(IO多路复用),核心部分仍为单线程(后用核心线程表示)。由此,将redis核心线程从IO中解脱出来,专门进行内存数据库的读写,接收和发送由多线程这个管家来完成。
如上图所示,selector线程阻塞监听客户端的请求(修改和查询数据库),当有请求准备完成后,通知核心线程去操作数据库,操作的结果也通过建立好的通道返回给客户端。
上图本质上就是NIO+核心线程的模型,核心线程读写数据库。
再说一下为什么核心线程不用多线程,现在服务器都是多CPU、多核心,只要处理好线程安全问题,完全可以使用多线程处理核心业务(数据库读写):一方面,现有的Redis处理的读写速度是8~10W每秒,即使再堆CPU内核和线程也无法继续提高数据库操作速度,本质原因是短板效应------内存和IO的速度远低于CPU,尽管通过多路复用IO已经缓解了IO部分的效率问题;另一方面,线程的切换和线程安全问题可能会带来性能开销和复杂的程序设计问题。
4.Redis事务
Redis中指令都是单线程执行的,因此每个指令原子的、隔离的和持久性的。当需要将多个指令串起来执行时,需要用到Redis事务机制。
Redis中的事务属于伪事务,不具备原子性和一致性,仅具备隔离性和持久性。通过Redis事务API可以将多个命令串起来输入到Redis中,Redis会依次执行这些指令,当有指令执行失败时,不会停止和回滚,而是忽略异常继续执行下一个指令,从而丧失了原子性和一致性。多个客户端向Redis服务器发送事务指令时,Redis服务器会按照提交给Redis的顺序依次执行各个事务( Redis的事务可以保证一个事务内的命令依次执行而不被其他命令插入 ),从而保证了事务之间的隔离性和持久性。
Redis事务通过MULTI、EXEC、DISCARD三个指令来支持事务,
MULTI与DISCARD指令组合时,不会执行中间的指令(直接丢弃);MULTI与EXEC执行组合,将多个读写操作进行串联:
shell
53:0>multi
"OK"
53:0>set "key1" "value1"
"QUEUED"
53:0>set "key2" "value2"
"QUEUED"
53:0>exec
1) "OK"
2) "OK"
3) "OK"
4) "OK"
5) "OK"
53:0>get key1
"value1"
53:0>get key2
"value2"
53:0>
在执行事务前,可以通过watch监听感兴趣的键是否发送变化。watch属于乐观锁,当事务命令被执行时,会比较watch关注的键是否在此阶段(watch执行到事务执行之间)是否发送了变化;如果有变化,则取消执行事务;否则会正常执行事务;最后(无论是否执行事务)取消watch监控。
5.数据过期
redis可以对数据添加过期时间,数据过期后会被redis删除。关于过期数据的删除, 存在以下三种策略:
[1] 立即删除:
当数据过期后,立即被redis删除;可以保证内存的最大新鲜度,即内存里的数据都是有效的。由于每条数据过期,都会执行删除操作,极大占用了CPU资源。
[2] 惰性删除
当数据过期后,不会立即删除,直到下次数据被访问时才会删除。该策略对CPU资源比较友好,但是内存中会存在大量已过期的数据。
[3] 定期删除
立即删除对内存友好而CPU不友好,而惰性删除对CPU友好而内存不友好,定期删除策略在二者中做了折中处理。
定期扫描过期数据,扫描到过期数据时立即删除,定期之外的数据仍采用惰性删除策略。
6.内存淘汰
Redis可通过maxmemory配置修改Redis占有内存的最大值。 当内存告急时(达到maxmemory)时,Redis通过内存淘汰机制保证服务继续运行。根据是否删除、键的选择(是否设置过期时间)、删除方式(随机、LFU、LRU)的组合,有如下8种淘汰策略(通过maxmemory-policy进行配置):
[1] noeviction:默认的配置,支持查看和删除数据,拒绝所有写入操作并返回客户端错误消息;
[2] volatile-ttl :在设置了过期时间的key中, 淘汰过期时间剩余最短的;
[3] volatile-lru : 在设置了过期时间的key中, 根据LRU淘汰;
[4] volatile-lfu : 在设置了过期时间的key中, 根据LFU淘汰;
[5] volatile-random: 在设置了过期时间的key种, 随机淘汰;
[6] allkeys-lru : 从所有 key 中使用 LRU 算法进行淘汰;
[7] allkeys-lfu : 从所有 key 中使用 LFU 算法进行淘汰;
[8] allkeys-random: 从所有 key中随机淘汰。
其中: LRU(Least Recently Used)表示最近最少使用,LFU(Least Frequently Used)表示最不经常使用。