全部产品
Search
文档中心

负载均衡:ALB使用WebSocket协议实现信息实时推送

更新时间:Nov 20, 2024

WebSocket是一种在单个TCP连接上提供全双工通信渠道的网络协议。WebSocket的设计使得客户端和服务器之间可以实现持久连接,都能够主动地向对方发送数据或接收数据,减少了频繁建立连接的开销和延迟,这通常比传统的HTTP请求和响应模式更高效。WebSocket主要应用于需要实时通信功能的场景,ALB默认支持WebSocket协议。

WebSocket简介

为什么使用WebSocket

随着互联网技术的迅速发展,Web应用的多样化趋势日益显著,其中不少应用场景,例如直播间聊天室、实时弹幕等,都需要服务器具备实时推送数据的能力。传统的实现方式是通过轮询技术,即客户端浏览器在固定的时间间隔(例如每1秒)向服务器发起HTTP请求,服务器随后将最新数据返回给客户端。然而,这种模式存在显著的不足,客户端需要频繁地发起请求,而HTTP请求的头部信息通常较长,有效数据却相对较少,这不仅增加了服务器的负担,也造成了带宽资源的极大浪费。

为了解决这些问题,HTML5引入了WebSocket协议,它为服务器与客户端之间的通信提供了更为高效的解决方案。WebSocket协议支持全双工通信,这意味着服务器和客户端可以同时进行数据的发送和接收,从而允许服务器在有新数据时主动推送给客户端,无需客户端不断轮询。这种双向实时通信机制显著提高了数据传输的效率,减少不必要的网络请求,有效节省服务器资源和带宽,同时为用户带来更为流畅和实时的交互体验。

WebSocket协议的特性

WebSocket通信前,首先客户端与服务器要进行TCP三次握手连接,然后进行一次叫做“握手”的特殊HTTP请求进行协议升级,完成协议升级后原始的HTTP连接被升级到WebSocket连接。在协议升级后,客户端和服务器之间的通信将使用WebSocket协议进行而不再是HTTP,可以在同一个WebSocket连接上进行双向通信。

WebSocket连接一旦经过握手协商成功建立,便能维持活跃状态,使得双方能如同使用原始套接字(Socket)那样进行连续不断的双向数据传输,而不必为每个通信回合重新发起连接或等待确认。通过WebSocket,客户端和服务器之间得以建立一种持久、低延迟的连接,极大地提升了数据交换效率。

image

WebSocket通过数据帧进行通信,它有自己的帧协议格式,头信息更简洁,数据可以作为文本或二进制传输。这种方式减少了持久连接上额外的协议开销,允许更高效的网络交互,能够在节省服务器资源和带宽的同时,提供更优质的实时互动体验。

关于WebSocket协议的更多信息,可参考官方文档The WebSocket Protocol

WebSocket应用场景

WebSocket主要适用于需要快速、实时的双向通信的应用场景,例如AI应用、在线聊天室、实时通知系统、多人在线游戏、实时市场信息推送等。

场景示例

某公司需要在阿里云上部署Web在线聊天应用,用户可以通过访问域名来接入后端服务进行实时交流。该应用由于其即时通讯的特性,要求用户之间的信息传递必须具备低延迟、高效率和双向实时的特点。

该公司的网站服务面临的挑战是高并发与长连接管理。随着用户数量的增长,传统的HTTP模式无法满足大量用户同时在线并保持实时通信的需求,因为每次通信都需要重新建立连接,这会导致服务器压力剧增且性能低下。

在这个场景下,选用ALB结合WebSocket协议,能够有效解决高并发下的长连接管理问题。通过后端服务器组多服务器部署WebSocket应用程序,并使用Redis进行消息同步,确保服务的高可用性,从而为在线聊天室应用提供了一个可靠、高效的实时信息推送解决方案。

image

注意事项

ALB的HTTP监听默认支持WebSocket协议。ALB默认支持热更新,即配置变更时不会影响已有长连接。

使用时需要注意如下事项:

  • 若ALB与后端服务器的连接采用某个版本的HTTP协议(例如HTTP/1.1),建议后端服务器采用支持同样HTTP协议版本的Web Server。

  • HTTP监听的默认连接请求超时时间为60秒,即如果ALB与后端服务超过60秒无消息交互,会主动断开连接。

    • 如果60秒无法满足您的需求,您可以通过修改监听的连接请求超时时间字段,调整该时间值。

    • 如果需要维持连接一直不中断,需要主动实现保活机制,每60秒内进行一次报文交互。

前提条件

  • 已创建公网ALB实例。具体操作,请参见创建应用型负载均衡

  • 已准备3个云服务器ECS01、ECS02、ECS03。

    • ECS01、ECS02用于部署WebSocket应用程序,ECS03用于部署Redis。

    • 本文示例中服务器操作系统统一为CentOS7.9。

    • ECS01、ECS02、ECS03建议处于同一安全组中。如果分配了不同的安全组,注意互相放行服务器需要通信的端口。

  • 已经注册域名并完成备案。具体操作,请参见注册阿里云域名ICP备案

操作步骤

步骤一:部署服务

您需要在您的ECS03服务器中部署Redis,在ECS01、ECS02服务器中部署WebSocket应用程序。

本文以CentOS 7.9为示例,演示使用Python快速部署一个简易的在线聊天室测试服务。示例仅供参考,实际使用过程中以您自己开发的程序和服务为准。

在ECS03部署Redis服务

  1. 登录ECS03服务器后台。

  2. 复制粘贴如下命令,并执行,完成Redis部署与配置。

    # 安装 EPEL (Extra Packages for Enterprise Linux)
    sudo yum install epel-release -y
    
    # 安装 Redis
    sudo yum install redis -y
    
    # 启动并启用 Redis 服务
    sudo systemctl start redis
    sudo systemctl enable redis
    
    # 检查并编辑 Redis 配置文件,允许远程连接
    sudo sed -i 's/^bind 127.0.0.1$/bind 0.0.0.0/' /etc/redis.conf
    sudo sed -i 's/^protected-mode yes/protected-mode no/' /etc/redis.conf
    
    # 重启 Redis 服务以使更改生效
    sudo systemctl restart redis
    
    # 检查 Redis 是否运行
    sudo systemctl status redis
    
  3. 命令执行无报错,并且命令运行完成后,返回如下信息并显示Redis服务为active(running)状态,表示部署与配置成功。

    image

在ECS01中部署WebSocket应用程序

  1. 登录ECS01服务器后台。

  2. 执行sudo pip3 install flask flask-socketio flask-cors redis,安装依赖库。

  3. 执行vi ECS01_ws.py,按i键进入编辑模式。

  4. 复制并粘贴如下代码:

    部署测试服务代码参考

    说明

    注意第13行的redis_url中的IP地址,需要修改为Redis服务器的IP地址,即ECS03的IP地址。

    import os
    import redis
    from flask import Flask, render_template, request
    from flask_cors import CORS
    from flask_socketio import SocketIO, emit, disconnect
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'secret!'
    # 启用跨域资源共享(CORS)
    CORS(app)
    
    # 配置 Redis 作为消息队列和状态存储
    redis_url = "redis://192.168.*.*:6379/0"  # 替换为你的 Redis 服务器 IP
    redis_client = redis.StrictRedis.from_url(redis_url)
    
    # 日志级别增加为 DEBUG 以便于调试
    socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
    
    SESSION_PREFIX = "session:"
    
    
    def set_session_data(session_id, key, value):
        redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
    
    
    def get_session_data(session_id, key):
        return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
    
    
    def delete_session_data(session_id):
        redis_client.delete(f"{SESSION_PREFIX}{session_id}")
    
    
    @app.route('/')
    def index():
        return render_template('index.html')
    
    
    @socketio.on('connect')
    def handle_connect():
        try:
            session_id = request.sid  # 获取客户端的 session ID
            print(f"Session {session_id} connected.")
            welcome_message = "您已进入聊天室!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # 初始化用户名为空
        except Exception as e:
            print(f"Error during connection: {str(e)}")
    
    
    @socketio.on('disconnect')
    def handle_disconnect():
        try:
            session_id = request.sid
            username = get_session_data(session_id, "username")
            if username:
                username = username.decode()
                leave_message = f"{username} 已离开聊天室。"
                emit('message', leave_message, broadcast=True)
                print(leave_message)
            delete_session_data(session_id)
            print(f"Session {session_id} disconnected.")
        except Exception as e:
            print(f"Error during disconnection: {str(e)}")
    
    
    @socketio.on('set_username')
    def handle_set_username(username):
        session_id = request.sid
        set_session_data(session_id, "username", username)
        print(f"客户端 {session_id} 的用户名设置为 {username}")
        emit('message', f"您的用户名已设置为:{username}")
    
    
    @socketio.on('message')
    def handle_message(msg):
        session_id = request.sid
        username = get_session_data(session_id, "username")
        if username:
            username = username.decode()
            formatted_message = f"{username}:{msg}"
            emit('message', formatted_message, broadcast=True)
            print(formatted_message)
        else:
            warning_message = "发送消息失败:请先设置用户名。"
            emit('message', warning_message)
            print(warning_message)
    
    
    if __name__ == '__main__':
        # 确保存在 templates 目录
        if not os.path.exists('templates'):
            os.makedirs('templates')
    
        # 使用 Flask 模板(index.html)
        html_code = '''<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>聊天室</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                margin: 0;
                padding: 0;
                background-color: #f0f0f0;
            }
            h1 {
                color: #333;
            }
            .chat-container {
                width: 90%;
                max-width: 600px;
                background: white;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
            .user-container, .message-container {
                display: flex;
                margin-bottom: 10px;
            }
            .user-container input, .message-container input {
                flex: 1;
                padding: 10px;
                margin-right: 10px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            .message-container {
                margin-top: 10px;
            }
            button {
                padding: 10px;
                background-color: #0056b3;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }
            button:hover {
                background-color: #004099;
            }
            #messages {
                border: 1px solid #ccc;
                padding: 10px;
                height: 300px;
                overflow-y: scroll;
                margin-bottom: 10px;
                border-radius: 4px;
                background-color: #f9f9f9;
            }
        </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    </head>
    <body>
        <h1>在线聊天室</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="请输入用户名">
                <button onclick="setUsername()">设置用户名</button>
            </div>
            <div id="messages"></div>
            <div class="message-container">
                <input type="text" id="myMessage" autocomplete="off" placeholder="请输入消息...">
                <button onclick="sendMessage()">发送</button>
            </div>
        </div>
        <script>
            var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
            var usernameSet = false;
            socket.on('connect', function() {
                console.log("Connected to the server!");
                socket.on('message', function(msg){
                    $('#messages').append($('<div>').text(msg));
                    $('#messages').scrollTop($('#messages')[0].scrollHeight);
                });
            });
            function setUsername() {
                var username = $('#username').val();
                if (username) {
                    socket.emit('set_username', username);
                    usernameSet = true;  // 设置用户名标识
                } else {
                    alert("用户名不能为空!");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("消息不能为空!");
                    }
                } else {
                    alert("请先设置用户名!");
                }
            }
        </script>
    </body>
    </html>
    '''
    
        # 将模板保存到文件
        with open('templates/index.html', 'w') as file:
            file.write(html_code)
    
        socketio.run(app, host='0.0.0.0', port=5000)
    

  5. Esc键,输入:wq保存修改。

  6. 执行sudo python3 ECS01_ws.py命令,运行脚本。

  7. 当最后显示如下执行结果时,表示WebSocket应用程序已启动,端口为5000。

    Server initialized for threading.
     * Serving Flask app 'ECS01_ws' (lazy loading)
     * Environment: production
       WARNING: This is a development server. Do not use it in a production deployment.
       Use a production WSGI server instead.
     * Debug mode: off
     * Running on all addresses.
       WARNING: This is a development server. Do not use it in a production deployment.
     * Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
    

    如果出现启动失败,需要排查下端口是否已被占用,或者命令及代码是否复制粘贴错误。

在ECS02中部署WebSocket应用程序

  1. 登录ECS02服务器后台。

  2. 执行sudo pip3 install flask flask-socketio flask-cors redis,安装依赖库。

  3. 执行vi ECS02_ws.py,按i键进入编辑模式。

  4. 复制并粘贴如下代码:

    部署测试服务代码参考

    说明

    注意第13行的redis_url中的IP地址,需要修改为Redis服务器的IP地址,即ECS03的IP地址。

    import os
    import redis
    from flask import Flask, render_template, request
    from flask_cors import CORS
    from flask_socketio import SocketIO, emit, disconnect
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'secret!'
    # 启用跨域资源共享(CORS)
    CORS(app)
    
    # 配置 Redis 作为消息队列和状态存储
    redis_url = "redis://192.168.*.*:6379/0"  # 替换为你的 Redis 服务器 IP
    redis_client = redis.StrictRedis.from_url(redis_url)
    
    # 日志级别增加为 DEBUG 以便于调试
    socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
    
    SESSION_PREFIX = "session:"
    
    
    def set_session_data(session_id, key, value):
        redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
    
    
    def get_session_data(session_id, key):
        return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
    
    
    def delete_session_data(session_id):
        redis_client.delete(f"{SESSION_PREFIX}{session_id}")
    
    
    @app.route('/')
    def index():
        return render_template('index.html')
    
    
    @socketio.on('connect')
    def handle_connect():
        try:
            session_id = request.sid  # 获取客户端的 session ID
            print(f"Session {session_id} connected.")
            welcome_message = "您已进入聊天室!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # 初始化用户名为空
        except Exception as e:
            print(f"Error during connection: {str(e)}")
    
    
    @socketio.on('disconnect')
    def handle_disconnect():
        try:
            session_id = request.sid
            username = get_session_data(session_id, "username")
            if username:
                username = username.decode()
                leave_message = f"{username} 已离开聊天室。"
                emit('message', leave_message, broadcast=True)
                print(leave_message)
            delete_session_data(session_id)
            print(f"Session {session_id} disconnected.")
        except Exception as e:
            print(f"Error during disconnection: {str(e)}")
    
    
    @socketio.on('set_username')
    def handle_set_username(username):
        session_id = request.sid
        set_session_data(session_id, "username", username)
        print(f"客户端 {session_id} 的用户名设置为 {username}")
        emit('message', f"您的用户名已设置为:{username}")
    
    
    @socketio.on('message')
    def handle_message(msg):
        session_id = request.sid
        username = get_session_data(session_id, "username")
        if username:
            username = username.decode()
            formatted_message = f"{username}:{msg}"
            emit('message', formatted_message, broadcast=True)
            print(formatted_message)
        else:
            warning_message = "发送消息失败:请先设置用户名。"
            emit('message', warning_message)
            print(warning_message)
    
    
    if __name__ == '__main__':
        # 确保存在 templates 目录
        if not os.path.exists('templates'):
            os.makedirs('templates')
    
        # 使用 Flask 模板(index.html)
        html_code = '''<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>聊天室</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                margin: 0;
                padding: 0;
                background-color: #f0f0f0;
            }
            h1 {
                color: #333;
            }
            .chat-container {
                width: 90%;
                max-width: 600px;
                background: white;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
            .user-container, .message-container {
                display: flex;
                margin-bottom: 10px;
            }
            .user-container input, .message-container input {
                flex: 1;
                padding: 10px;
                margin-right: 10px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            .message-container {
                margin-top: 10px;
            }
            button {
                padding: 10px;
                background-color: #0056b3;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }
            button:hover {
                background-color: #004099;
            }
            #messages {
                border: 1px solid #ccc;
                padding: 10px;
                height: 300px;
                overflow-y: scroll;
                margin-bottom: 10px;
                border-radius: 4px;
                background-color: #f9f9f9;
            }
        </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    </head>
    <body>
        <h1>在线聊天室</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="请输入用户名">
                <button onclick="setUsername()">设置用户名</button>
            </div>
            <div id="messages"></div>
            <div class="message-container">
                <input type="text" id="myMessage" autocomplete="off" placeholder="请输入消息...">
                <button onclick="sendMessage()">发送</button>
            </div>
        </div>
        <script>
            var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
            var usernameSet = false;
            socket.on('connect', function() {
                console.log("Connected to the server!");
                socket.on('message', function(msg){
                    $('#messages').append($('<div>').text(msg));
                    $('#messages').scrollTop($('#messages')[0].scrollHeight);
                });
            });
            function setUsername() {
                var username = $('#username').val();
                if (username) {
                    socket.emit('set_username', username);
                    usernameSet = true;  // 设置用户名标识
                } else {
                    alert("用户名不能为空!");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("消息不能为空!");
                    }
                } else {
                    alert("请先设置用户名!");
                }
            }
        </script>
    </body>
    </html>
    '''
    
        # 将模板保存到文件
        with open('templates/index.html', 'w') as file:
            file.write(html_code)
    
        socketio.run(app, host='0.0.0.0', port=5000)
    

  5. Esc键,输入:wq保存修改。

  6. 执行sudo python3 ECS02_ws.py命令,运行脚本。

  7. 当最后显示如下执行结果时,表示WebSocket应用程序已启动,端口为5000。

    Server initialized for threading.
     * Serving Flask app 'ECS02_ws' (lazy loading)
     * Environment: production
       WARNING: This is a development server. Do not use it in a production deployment.
       Use a production WSGI server instead.
     * Debug mode: off
     * Running on all addresses.
       WARNING: This is a development server. Do not use it in a production deployment.
     * Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
    

    如果出现启动失败,需要排查下端口是否已被占用,或者命令及代码是否复制粘贴错误。

步骤二:配置服务器组

  1. 登录应用型负载均衡ALB控制台

  2. 在顶部菜单栏,选择实例所属的地域。

  3. 在左侧导航栏,选择服务器组

  4. 单击创建服务器组。在创建服务器组页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击创建

    配置

    说明

    服务器组类型

    选择服务器类型

    VPC

    选择后端服务器即ECS01、ECS02所在的VPC。

    后端服务器需要与ALB实例处于同一VPC中。

  5. 在弹出的服务器组创建成功窗口中,单击添加后端服务器

  6. 添加后端服务器配置页面,选中ECS01与ECS02并完成添加,注意端口需要配置为WebSocket应用程序端口。本文代码示例中WebSocket应用程序端口为5000。

步骤三:配置HTTP监听

  1. 登录应用型负载均衡ALB控制台

  2. 在顶部菜单栏,选择实例所属的地域。

  3. 在左侧导航栏,选择实例

  4. 实例页面,找到目标实例,然后在操作列单击创建监听

  5. 配置监听页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击下一步

    配置

    说明

    选择监听协议

    选择HTTP

    监听端口

    本文配置端口5000。

  6. 选择服务器组页面配置以下信息,其他参数可保持默认值或根据实际情况修改。完成后单击下一步

    配置

    说明

    选择服务器组

    选择此前已创建好的服务器组。

  7. 配置审核页面,检查配置参数是否有误,无误的话单击提交,等待监听创建完成。

步骤四:配置域名解析

实际业务场景中,建议您使用自有域名,通过CNAME解析的方式将自有域名指向ALB实例域名。

  1. 在左侧导航栏,选择应用型负载均衡 ALB > 实例

  2. 实例页面,复制已创建的ALB实例的DNS名称。

  3. 执行以下步骤添加CNAME解析记录。

    说明

    对于非阿里云注册域名,需先将域名添加到云解析控制台,才可以进行域名解析设置。具体操作,请参见域名管理。如果您是阿里云注册的域名,请直接执行以下步骤。

    1. 登录域名解析控制台

    2. 权威域名解析页面,找到目标域名,在操作列单击解析设置

    3. 解析设置页面,单击添加记录

    4. 添加记录面板,配置以下信息完成CNAME解析配置,然后单击确定

      配置

      说明

      记录类型

      在下拉列表中选择CNAME

      主机记录

      您的域名的前缀。本文输入@

      说明

      创建域名为根域名时,主机记录为@

      解析请求来源

      选择默认。

      记录值

      输入域名对应的CNAME地址,即您复制的ALB实例的DNS名称。

      TTL

      全称Time To Live,表示DNS记录在DNS服务器上的缓存时间,本文使用默认值。

步骤五:结果验证

准备2个不同IP地址的能够访问公网的终端电脑,通过在浏览器输入聊天消息并查看效果,验证ALB使用WebSocket协议实现信息实时推送。

  1. 在浏览器中输入http://域名:5000,访问在线聊天室应用。

    页面访问成功示例:

    image

    如果您打开了浏览器开发者工具,您可以在网络Network页签看到,浏览器已经在使用WebSocket协议进行通信。

    image

  2. 输入用户名用于后续聊天交互,完成后单击设置用户名

  3. 在不同终端电脑中,分别输入多条聊天消息并单击发送,进行测试。

    所有浏览器中,均可以实时收到消息。

    image

  4. 如上验证过程表明,通过ALB使用WebSocket协议实现了信息实时推送,并且实现了高可用。

常见问题

如何使用WebSocket Secure协议?

WebSocket Secure是WebSocket协议的加密版本。

HTTPS监听默认支持WebSocket Secure协议。您在配置监听时,选择使用HTTPS监听,即可使用WebSocket Secure协议。

使用WebSocket收费吗?

WebSocket和WebSocket Secure协议不额外收取费用。

哪些地域支持WebSocket?

ALB支持的所有地域,都已支持WebSocket和WebSocket Secure。

相关文档

本文的示例中使用了在ECS部署Redis的简单方式,便于您做业务测试,如果Redis服务器出现问题可能造成系统单点故障。在实际生产环境中,建议您使用云数据库 Tair(兼容 Redis),提升应用系统整体高可用性。云数据库Tair(兼容Redis)如何快速入门?