在开发的过程中,经常会碰到产品的这样一个要求,界面上的数据要实时展示。像一些全局性的数据,或者业务交集较少的数据可以实时去数据库查询,但是像一些列表类型的、用户访问量大的数据,不适合实时去查询。之前碰到的一个情况是这样的,如下图
帖子列也要展示封面图、帖子标题、帖子标签、用户头像、用户昵称、点赞数、用户属性等等字段。其中,点赞功能的操作发生概率很大,而运营团队做活动,对实时性的要求很高,需要让用户快速定位到热帖是哪个,进而参与话题活动等。
初次做的时候,是拒绝这个功能的(不知道怎么去好的实现),因为访问量大,而且要实时查询数据库的数据,会给社区业务带来一些性能上的影响,所以拒绝了产品的功能要求。按照我们的现有想法来做。
mysql查询 + redis缓存 + 代码优化实现列表的展示。查数据库时精确字段,排除冗余业务字段;用redis做了二级的数据缓存,一级是社区首页的全部数据(入口也),二级是板块、帖子、广告等数据的缓存。针对帖子的发表时间、热度、精选、点赞等不同维度都做了缓存。
现在呢,想想这个功能,可能会有几种方式去做。
- ES处理。整个项目后期增加了搜索引擎es,方便快速地查询帖子。在针对帖子的创建、修改等操作都会对es索引进行同步修改。由于ES的特性本身就适合查询(特性是全文检索、存储、分析),速度很快,所以将数据库查询改为 nosql查询是可以达到这个目的的(实时查询ES,我这里的测试,帖子索引数量是百万级,更高的没试过)。可能碰到的问题就是处理好,并发修改时的version版本号问题。(这种需要,客户端不断请求服务端数据),ELK相关请看《ELK学习》
再后来,了解到WebSocket协议,那感觉,这个更合适去处理实时性高的场景。
- WebSocket处理
WebSocketWebSocket 是 HTML5 开始提供的一种在单个 TCP连接上进行全双工通讯的协议。能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
这种方式不需要客户端轮询请求服务器端。只要首次建立链接之后,服务器端可以推送消息。Websocket 通过HTTP/1.1 协议的101状态码进行握手。
websocket的pom引用
<dependencies>
<!-- https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
webSocket的服务端、AWT客户端、H5客户端实现
在官方的git地址中,有很多的例子,请自行查阅
https://github.com/TooTallNate/Java-WebSocket/tree/master/src/main/example
1 .webSocket服务端代码
创建连接并监听事件
package com.chl.websocket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
/**
* webSocket.jar 使用来自以下地址
* https://repo1.maven.org/maven2/org/java-websocket/Java-WebSocket/1.4.0/
* @author chenhailong
*/
public class WebSocketTest extends WebSocketServer {
/**
* 构造函数
* @param port
* @throws UnknownHostException
*/
public WebSocketTest( int port ) throws UnknownHostException {
super( new InetSocketAddress( port ) );
}
/**
* 构造函数
* @param address
*/
public WebSocketTest( InetSocketAddress address ) {
super( address );
}
/**
* 打开连接时的监听事件
* conn.send(msg); //发送信息给最新的客户端
* broadcast(); //广播模式,发送给所有连接此端口的客户端
*/
@Override
public void onOpen( WebSocket conn, ClientHandshake handshake ) {
conn.send("欢迎访问webSocket服务!");
broadcast( "new connection: " + handshake.getResourceDescriptor() );
System.out.println( conn.getRemoteSocketAddress().getAddress().getHostAddress() + " 来到了这个聊天室!" );
}
/**
* 关闭连接时的监听事件
*
*/
@Override
public void onClose( WebSocket conn, int code, String reason, boolean remote ) {
broadcast("连接为:"+ conn + "的端已经离开了这个聊天室 !" );
System.out.println("连接为:"+ conn + "的端已经离开了这个聊天室 !");
}
/**
* 监听字符串消息,并广播
*/
@Override
public void onMessage( WebSocket conn, String message ) {
broadcast( message );
System.out.println("连接为:"+ conn + "的端发送消息: " + message );
}
/**
* 监听字节消息,并广播
*/
@Override
public void onMessage( WebSocket conn, ByteBuffer message ) {
broadcast( message.array() );
System.out.println("连接为:"+ conn + "的端发送消息: " + message );
}
/**
* main方法,创建并开启webSocket服务端, 轮询消息并输出
* @param args
* @throws InterruptedException
* @throws IOException
*/
public static void main( String[] args ) throws InterruptedException , IOException {
int port = 9999;
try {
port = Integer.parseInt( args[ 0 ] );
} catch ( Exception ex ) {
}
WebSocketTest s = new WebSocketTest( port );
s.start();
System.out.println( "WebSocket 服务端已经运行,端口为: " + s.getPort() );
//这里是控制台输入,可以更改为数据库读取或者nosql查询等
BufferedReader sysin = new BufferedReader( new InputStreamReader( System.in ) );
while ( true ) { //可以设置查询时间
String in = sysin.readLine();
s.broadcast( in );
if( in.equals( "exit" ) ) {
s.stop(1000);
break;
}
}
}
/**
* 监听错误事件
*/
@Override
public void onError( WebSocket conn, Exception ex ) {
ex.printStackTrace();
if( conn != null ) {
// some errors like port binding failed may not be assignable to a specific websocket
}
}
/**
* 监听开启事件
*/
@Override
public void onStart() {
System.out.println("WebSocket 服务端已经正常开启!");
setConnectionLostTimeout(0);
setConnectionLostTimeout(100);
}
}
2 .webSocket客户端AWT实现代码
简单类似聊天室功能,做连接、关闭、输入等范例
package com.chl.websocket;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.net.URI;
import java.net.URISyntaxException;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
/**
* 这里是官网的客户端例子,
* 利用awt组件做了一个聊天界面,可以开启、关闭webSocket连接。也可以接收其他端或服务端的消息并展示
* 为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking
* @author chenhailong
*
*/
public class WebSocketClientAWT extends JFrame implements ActionListener {
private static final long serialVersionUID = -6056260699202978657L;
private final JTextField uriField;
private final JButton connect;
private final JButton close;
private final JTextArea ta;
private final JTextField chatField;
@SuppressWarnings("rawtypes")
private final JComboBox draft;
private WebSocketClient cc;
/**
* 利用AWT组件构建一个聊天界面
* @param defaultlocation
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public WebSocketClientAWT( String defaultlocation ) {
super( "WebSocket 聊天客户端" );
Container c = getContentPane();
GridLayout layout = new GridLayout();
layout.setColumns( 1 );
layout.setRows( 6 );
c.setLayout( layout );
Draft[] drafts = { new Draft_6455() };
draft = new JComboBox( drafts );
c.add( draft );
uriField = new JTextField();
uriField.setText( defaultlocation );
c.add( uriField );
connect = new JButton( "建立连接!" );
connect.addActionListener( this );
c.add( connect );
close = new JButton( "关闭连接!" );
close.addActionListener( this );
close.setEnabled( false );
c.add( close );
JScrollPane scroll = new JScrollPane();
ta = new JTextArea();
scroll.setViewportView( ta );
c.add( scroll );
chatField = new JTextField();
chatField.setText( "" );
chatField.addActionListener( this );
c.add( chatField );
java.awt.Dimension d = new java.awt.Dimension( 300, 400 );
setPreferredSize( d );
setSize( d );
addWindowListener( new java.awt.event.WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
if( cc != null ) {
cc.close();
}
dispose();
}
} );
setLocationRelativeTo( null );
setVisible( true );
}
/**
* 事件监听
*/
public void actionPerformed( ActionEvent e ) {
if( e.getSource() == chatField ) {
if( cc != null ) {
cc.send( chatField.getText() );
chatField.setText( "" );
chatField.requestFocus();
}
} else if( e.getSource() == connect ) {
try {
// cc = new ChatClient(new URI(uriField.getText()), area, ( Draft ) draft.getSelectedItem() );
cc = new WebSocketClient( new URI( uriField.getText() ), (Draft) draft.getSelectedItem() ) {
@Override
public void onMessage( String message ) {
ta.append( "got: " + message + "\n" );
ta.setCaretPosition( ta.getDocument().getLength() );
}
@Override
public void onOpen( ServerHandshake handshake ) {
ta.append( "You are connected to ChatServer: " + getURI() + "\n" );
ta.setCaretPosition( ta.getDocument().getLength() );
}
@Override
public void onClose( int code, String reason, boolean remote ) {
ta.append( "You have been disconnected from: " + getURI() + "; Code: " + code + " " + reason + "\n" );
ta.setCaretPosition( ta.getDocument().getLength() );
connect.setEnabled( true );
uriField.setEditable( true );
draft.setEditable( true );
close.setEnabled( false );
}
@Override
public void onError( Exception ex ) {
ta.append( "Exception occured ...\n" + ex + "\n" );
ta.setCaretPosition( ta.getDocument().getLength() );
ex.printStackTrace();
connect.setEnabled( true );
uriField.setEditable( true );
draft.setEditable( true );
close.setEnabled( false );
}
};
close.setEnabled( true );
connect.setEnabled( false );
uriField.setEditable( false );
draft.setEditable( false );
cc.connect();
} catch ( URISyntaxException ex ) {
ta.append( uriField.getText() + " is not a valid WebSocket URI\n" );
}
} else if( e.getSource() == close ) {
cc.close();
}
}
public static void main( String[] args ) {
String location;
if( args.length != 0 ) { //有参是取这里
location = args[ 0 ];
System.out.println( "默认的服务端url为: \'" + location + "\'" );
} else {
location = "ws://localhost:9999";
System.out.println( "默认的服务端url为: \'" + location + "\'" );
}
new WebSocketClientAWT( location );
}
}
3.html5的js实现websocket
服务器推送数据,前端客户端可以正常监听处理
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket的html样例</title>
<script type="text/javascript">
//客户端的WebSocket连接
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://localhost:9999/");
//这是连接事件
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
//这是消息监听事件,在一个html标签中显示
ws.onmessage = function (evt)
{
var received_msg = evt.data;
var context = document.getElementById("show").innerText;
document.getElementById("show").innerText = context +"\n"+ received_msg;
};
//这是关闭事件(服务器关闭)
ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body onload="WebSocketTest()">
<br><hr> <div id="show"></div>
</body>
</html>
服务器端输出如下:
## 客户端连接时输出
127.0.0.1 来到了这个聊天室!
## 客户端发送消息时输出
连接为:org.java_websocket.WebSocketImpl@46775c78的端发送消息: 你好
连接为:org.java_websocket.WebSocketImpl@46775c78的端发送消息: 这是一个AWT界面
## 服务端输出内容,客户端也会显示
收到了
你是第一个来的客户
这只是websocket的简单实现,如果要实现帖子点赞数的实时展示,对于数据的更新还要做对应的处理。比如持久化及即时展现,还是要做些设计的。用户点赞的数据是经过内存,再持久化到数据库。还是直接到数据库,根据自身的情况做处理。