Redis 多路IO复用及原理
Jedis源码
Jedis jedis = new Jedis("192.169.4.4", 6379);
jedis.set("name","aa"); //socket
jedis.close();
底层是 this.socket = this.jedisSocketFactory.createSocket(); 创建了一个Socket
socket-->tcp协议--->操作系统的内核 os kernel
模拟redis服务端和客户端
服务端
//ServerSocket
public class RedisServer {
static byte[] bs = new byte[1024];
static ArrayList<Socket> socketList = null;
// BIO --> sun --->NIO
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(6379);
while (true){
/**
* 2.accept是阻塞的方法,程序已经放弃了CPU
*/
System.out.println("wait conn----------");
Socket clientSocket = serverSocket.accept();
System.out.println("conn sucess------");
/**
* 3.read方法是阻塞的方法
*/
System.out.println("wait data---------");
clientSocket.getInputStream().read(bs);
/**
* 4.如果此时,开启了两个客户端连接这个服务端。第二个是连接不上的,需要开启多线程实现。 BIO实现
* 使用单线程实现并发。(因为有的仅仅是连接,没有发送数据)
*/
System.out.println("data success------------");
System.out.println(new String(bs));
}
}
}
客户端1
public class SocketTest {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 6379);
// socket.getOutputStream().write("test".getBytes());
// socket.close();
//测试read方法是阻塞的方法
Scanner scanner = new Scanner(System.in);
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
socket.close();
}
}
客户端2
public class SocketTest {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 6379);
// socket.getOutputStream().write("test".getBytes());
// socket.close();
//测试read方法是阻塞的方法
Scanner scanner = new Scanner(System.in);
String next = scanner.next();
socket.getOutputStream().write(next.getBytes());
socket.close();
}
}
BIO实现的服务端
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(6379);
System.out.println("new ServerSocket(8080)");
while (true) {
final Socket clientSocket = serverSocket.accept();
System.out.println("cclient: " + clientSocket.getPort());
//没来一个连接就创建一个线程,无论这个连接是否会有io交互,实现空轮训。
new Thread(
() -> {
try {
InputStream in = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true) {
System.out.println(reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
).start();
}
}
}
NIO多路复用服务端实现
public class NIORedis {
static ByteBuffer byteBuffer = ByteBuffer.allocate(512);
static ArrayList<SocketChannel> socketList = new ArrayList<>();
public static void main(String[] args) throws Exception {
//ServerSocket serverSocket = new ServerSocket(6379);
//NIO设计理念
ServerSocketChannel serverSocket = ServerSocketChannel.open();
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6379);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false);//将accept方法设置成非阻塞
while (true){
//1.进来先判断我所有的连接,是不是有数据发送过来了,如果有就打印出来
for(SocketChannel socketChannel : socketList){
int read = socketChannel.read(byteBuffer);
if(read > 0){
System.out.println("read------- " + read);
byteBuffer.flip();
byte[] bs = new byte[read];
byteBuffer.get(bs);
String content = new String(bs);
System.out.println(content);
byteBuffer.flip();
}
}
//2.是不是有新的连接加入进来
SocketChannel clientSocket = serverSocket.accept();
if(clientSocket != null){
System.out.println("conn success------------");
clientSocket.configureBlocking(false);
socketList.add(clientSocket);
System.out.println("socketlist size == " + socketList.size());
}
}
}
}
NIO多路复用的测试
测试:
1)启动NIORedis.main
2) 运行 SocketTest.main 模拟一个客户端连接redis
3) 运行 SocketTest1.main 模拟另一个客户端连接redis
输出结果:
conn success------------
socketlist size == 1
conn success------------
socketlist size == 2
结论:
1)都能连接
2) 都能发送数据
存在的问题:
1)空循环太多,所有的连接都循环。
2)for循环代码即java运行在JVM中,JVM运行在os kernel中。
3)如果可以吧for循环交替给os kernel去执行,效率会很高。
操作系统的select和epollo解决NIO的问题
1)操作系统中有一个select函数。
int select(int ndfs,fd_set*readfds,fd_set*writefds,fd_set*exceptefds,struct timeval*timeout)
这个参数ndfs:等于客户端的连接数+ 原始的标准输入,标准输出,错误,监听,绑定等
socketList-->在jvm中作为一个入参,调用操作系统的select函数,会感知到哪些客户端有io流传递进来。
A)read流 B)write流
select(1w)--》也是全轮循。避免不了空轮循,所以select的性能也比较低。
会轮循,找到感知哪些socket右io流,就返回一个set集合。
2)epollo对比:
epollo_wait 可以自动感知io流的连接,从而只针对有io流的操作进行计算或者执行,不用全轮训,效率高。
JAVA线程与操作系统调用关系:
线程追踪
strace -ff -o ./test/james TestServer.java (BIOServer.java编译的)
strace:追踪一个程序启动内部有多少线程,每个线程对操作系统内核调用了什么事情
ff: 主线程和子线程fork
-o: 把结果输出
./test/james: 输出目录
TestServer: 哪个程序
BIOServer.java
public class BIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(6379);
System.out.println("new ServerSocket(8080)");
while (true) {
final Socket clientSocket = serverSocket.accept();
System.out.println("cclient: " + clientSocket.getPort());
new Thread(
() -> {
try {
InputStream in = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true) {
System.out.println(reader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
).start();
}
}
}
jps
21015 TestServer
cd /proc/21015/fd 得到:
0 -> /dev/pts/1
1 -> /dev/pts/1
2 -> /dev/pts/1
3 -> /xxx/jdk1.8/jre/lib/rt.jar
4 -> socket:[27385942]
5 -> socket:[12238532] 这个就是监听了tcp的端口8088
strace结果:
cd /usr/local/c/test
james.21015 #主线程
james.21016 #main方法中主方法的线程
james.21017
james.21018
james.21019
james.21020
james.21021
james.21022
james.21023
james.21024
此时还在监听,没有创建new Thread(),否则应该是11个线程。
task结果:
cd /proc/20215/task #每个线程对应的文件
21015 # clone了一个线程=21016 新来的线程都是clone的
21016
21017
21018
21019
21020
21021
21022
21023
redis多线程与单线程
redis6.0之前 单线程
多个客户端连接redis。其实是与操作系统kernel创建socket连接。kernel通过epollo能感知socket1和socket2有io数据发送过来,就会通知redis去对相关的连接操作。
真正计算操作的是一个单线程。worker线程,每一个操作,例如socket1中的set xx=>都对应了3个步骤: read io–>计算–>write io。一个线程串行执行后再去计算向下一个操作,get xx。
redis6.0之后 多线程
除了worker线程外,还有io线程(对于连接中有操作的连接都会开辟io线程)。
其中io线程负责: read io,write io
worker线程负责:计算。
Redis的key设计原则:
1)key短小:因为key也会存储到内存中。
2)唯一不重复
3)功能 业务 表明 作为开头
String类型
基础命令
1)最大不能超过512M
2)设置命令:
set age 23 ex 10 //10s后过期
setnx name test //不存在name时候,返回1设置成功,存在返回0失败。用途:实现分布式锁。
mset country china city beijing //批量设置,原子性的。减少和redis的连接,提升效率
订单系统:
下订单–>生成一个订单号(唯一id):自增账号。使用redis的自增。但是这种方式性能比较低,redis的并发(性能)可以达到10w连接处理,假设有10个订单挺尸需要得到一个自增主键,就会极大降低redis的使用性能。
解决:一次性从redis获取1000个自增账号,并且把这1000个账号缓存起来,然后使用。
Session共享
多个tomcat实现session共享,实现单点登录。
Hash类型
List类型
栈: 先进后出。lpush+lpop
队列:先进先出。lpush+rpop
阻塞队列:blocking-queue 如果队列不为空,就是和普通队列一样。如果队列为空,会阻塞在这里,等到有了数据就使用。
Set集合
可以用于抽奖
spop active:001 2 对key=active:001 中的元素抽取2个值。
可以用于用户–发送多条消息。每条消息–多个用户点赞。
Zset有序集合
每个元素绑定一个分值,用于排序。
Redis源码
redis是c语言开发的。
单节点redis的库有16个。分布式集群只有一个数据库。
./redis-server redis.conf
main函数:
重置参数,解析redis.conf,设置参数。
initServer():
Redis的进程是操作系统给的。
客户端的连接信息,放在clients中,就是一个list空间。
redis数据恢复:
Aop开关打开没有,打开了先恢复。
RDB恢复
Redis应用场景
涉及到支付金钱,一定不能用redis锁。会涉及到数据丢失的问题。为什么?
一般都是主从架构,从节点是不会工作的,等主节点挂了从节点就会工作。
用户1一把锁。往主节点redis中设置了lock,并且会吧这个lock同步到从节点中。但是在同步的过程中,主节点挂了,用户B往从节点中请求,就能获取到这个锁。造成不安全。
Redis锁:
4个用户,买票100张。
A用户往redis中先插入一个key,如果能插入成功,就可以去买票,100-1=99,买完之后吧这个key清除.此时B用户往redis中插入同样一个key,不能成功,需要等待A完成之后才行。