springboot的netty代碼實操

来源:https://www.cnblogs.com/warrenwt/p/18155613
-Advertisement-
Play Games

參考:https://www.cnblogs.com/mc-74120/p/13622008.html pom文件 <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> 啟動 ...


參考:https://www.cnblogs.com/mc-74120/p/13622008.html

pom文件

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

啟動類

@EnableFeignClients
@EnableDiscoveryClient
@EnableScheduling
@SpringBootApplication
@EnableAsync
public class ChimetaCoreApplication  implements CommandLineRunner{
    
    @Autowired
    private NettyServerListener nettyServerListener;
    
    public static void main(String[] args) {
        SpringApplication.run(ChimetaCoreApplication.class, args);
    }
    
    @Override
    public void run(String... args) throws Exception {
       
       nettyServerListener.start();
    }
}

服務端代碼的listener

package com.chimeta.netty;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.chimeta.netty.protobuf.ImProto;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

/**
 * 服務啟動監聽器
 *
 * @author mwan
 */
@Component
@Slf4j
public class NettyServerListener {
    /**
     * 創建bootstrap
     */
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    /**
     * BOSS
     */
    EventLoopGroup boss = new NioEventLoopGroup();
    /**
     * Worker
     */
    EventLoopGroup work = new NioEventLoopGroup();
    /**
     * 通道適配器
     */
    @Resource
    private ServerChannelHandlerAdapter channelHandlerAdapter;
    /**
     * 從配置中心獲取NETTY伺服器配置
     */
    @Value("${server.netty.port:10001}")
    private int NETTY_PORT;
    
    @Value("${server.netty.maxthreads:5000}")
    private int MAX_THREADS;

    /**
     * 關閉伺服器方法
     */
    @PreDestroy
    public void close() {
        log.info("關閉伺服器....");
        //優雅退出
        boss.shutdownGracefully();
        work.shutdownGracefully();
    }

    /**
     * 開啟及服務線程
     */
    public void start() {
        serverBootstrap.group(boss, work)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, MAX_THREADS) //最大客戶端連接數為1024  
                .handler(new LoggingHandler(LogLevel.INFO)).childOption(ChannelOption.SO_KEEPALIVE, true); ;
        try {
            //設置事件處理
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 下麵的每一個addLast都有自己的含義,需要每個都過一下
                    ch.pipeline().addLast(new IdleStateHandler(18,0,0));
                    ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
                    //ch.pipeline().addLast(new CustomProtobufInt32FrameDecoder());
                    ch.pipeline().addLast(new ProtobufDecoder(ImProto.ImMsg.getDefaultInstance()));
                    ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
                    //ch.pipeline().addLast(new CustomProtobufInt32LengthFieldPrepender());
                    ch.pipeline().addLast(new ProtobufEncoder());
                    // 業務處理
                    ch.pipeline().addLast(channelHandlerAdapter);
                }
            });
            log.info("netty伺服器在[{}]埠啟動監聽", NETTY_PORT);
            ChannelFuture f = serverBootstrap.bind(NETTY_PORT).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("[出現異常] 釋放資源", e);
            boss.shutdownGracefully();
            work.shutdownGracefully();
            log.info("服務已關閉!");
        }
    }
}

 ServerChannelHandlerAdapter處理類

package com.chimeta.netty;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.chimeta.netty.model.SessionCloseReason;
import com.chimeta.netty.protobuf.ImProto.ImMsg;
import com.chimeta.netty.util.ChannelUtils;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;

/**
 * 通信服務處理器
 */
@Component
@Sharable
@Slf4j
public class ServerChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
    /**
     * 註入請求分排器
     */
    @Autowired
    private MessageDispatcher messageDispatcher;
    
    @Autowired
    private DeviceSessionManager sessionManager;

    /** 用來記錄當前線上連接數。應該把它設計成線程安全的。  */
    //private AtomicInteger sessionCount = new AtomicInteger(0);
    
    @Override  
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {  
        super.handlerAdded(ctx); 
        
        if (!ChannelUtils.addChannelSession(ctx.channel(), new IoSession(ctx.channel()))) {
          ctx.channel().close();
          log.error("Duplicate session,IP=[{}]",ChannelUtils.getRemoteIp(ctx.channel()));
       }     

        //String server_ip = NetworkUtils.getRealIp();//獲得本機IP
        // 緩存計數器加1
    }  
      
    @Override  
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {  
        super.handlerRemoved(ctx); 

        // 緩存計數器減1
        //String server_ip = NetworkUtils.getRealIp();//獲得本機IP
        log.info(ctx.channel().id()+"離開了");  
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        
        ImMsg gameMessage = (ImMsg)msg;
        final Channel channel = ctx.channel();
       IoSession session = ChannelUtils.getSessionBy(channel);
       if(session.isHeartbeated()) {
          session.setHeartbeated(false);
       }
       
       String deviceCode="";
       if(session.getDevice() != null && StringUtils.isNotBlank(session.getDevice().getDeviceCode())) {
          deviceCode = session.getDevice().getDeviceCode();
       }
//     if(!MessagingConst.TYPE_UPOS_REQUEST.equals(gameMessage.getMsg().getTypeUrl())) {
          try {
             log.info("Inbound message is :" + JsonFormat.printer().usingTypeRegistry(DeviceSessionManager.typeRegistry).print(gameMessage.toBuilder())
                   + ", from device " + deviceCode);
          } catch (InvalidProtocolBufferException e) {
             log.info("Inbound message is :" + gameMessage.toString());
          }
//     }
       
       messageDispatcher.dispatch(gameMessage, session);
    }
     
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {  
        ctx.flush();  
    } 
    
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  
            throws Exception {  
        
        log.error("通信發生異常:", cause);
        ctx.close();   
    } 
    
    /**
     * 一段時間未進行讀寫操作 回調
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        /*心跳處理*/
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                /*讀超時*/
                log.info("READER_IDLE read overtime,close session");
                final Channel channel = ctx.channel();
               IoSession session = ChannelUtils.getSessionBy(channel);
                
             /*
              * if(messageDispatcher.sendHeartbeat(session) == false) { //如果心跳檢測失敗,則連接異常,主動斷開
              * session.setSessionCloseReason(SessionCloseReason.OVER_TIME); ctx.close(); };
              */
               
               session.setSessionCloseReason(SessionCloseReason.OVER_TIME);
              ctx.close();
                
            } else if (event.state() == IdleState.WRITER_IDLE) {
                /*寫超時*/   
                log.info("WRITER_IDLE 寫超時");
            } else if (event.state() == IdleState.ALL_IDLE) {
                /*總超時*/
                log.info("ALL_IDLE 總超時");
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        sessionManager.unregisterUserContext(ctx.channel());
        log.info(ctx.channel().id() + "已掉線!");
        // 這裡加入玩家的掉線處理
        ctx.close();

    }

}

MessageDispatcher分派各個處理器

package com.chimeta.netty;

import com.chimeta.netty.service.TerminalService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import com.chimeta.netty.constant.MessagingConst;
import com.chimeta.netty.model.SessionCloseReason;
import com.chimeta.netty.protobuf.ImProto.ImMsg;
import com.chimeta.netty.service.LoginService;
import com.chimeta.netty.util.MessageBuilder;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Resource;

/**
 * 請求分排器
 */
@Component
@Slf4j
public class MessageDispatcher{
    
    @Autowired
    private LoginService loginService;

    @Resource
    private TerminalService terminalService;
    
    /**
     * 消息分發處理
     *
     * @param gameMsg
     * @throws InvalidProtocolBufferException 
     */
    @Async
    public void dispatch(ImMsg imMsg, IoSession currSession) throws InvalidProtocolBufferException {

        if(imMsg.getId() < 0) {
           currSession.sendMessage(MessageBuilder.buildErrorResponse(imMsg, MessagingConst.RESPONSE_ERR_CODE_400, "Invalid message!"));
           return;
        }
        //log.info("接收到的消息TypeUrl是: "+imMsg.getMsg().getTypeUrl());
        switch(imMsg.getMsg().getTypeUrl()) {
        
            case MessagingConst.TYPE_ONLINE_REQUEST:
               // 處理設備上線請求
               loginService.doLogin(imMsg, currSession);
               break;
            case MessagingConst.TYPE_USER_LOGON_REQUEST:
               // 處理請求
               loginService.doUserLogon(imMsg, currSession);
               break;
            case MessagingConst.TYPE_USER_LOGOFF_REQUEST:
               // 處理請求
               loginService.doUserLogoff(imMsg, currSession);
               break;
          case MessagingConst.TYPE_TERMINAL_STATE_REQUEST:
                // 我寫的
             terminalService.multiInsert(imMsg, currSession);
             break;
            default:
               if(currSession != null) {
                  // 返回客戶端發來的心跳消息
                  responseHeartbeat(imMsg, currSession);
               }
               break;
        }
    }
    
    /**
     * 發送心跳包消息
     * @param gameMsg
     * @param currSession
     * @return
     */
    public boolean sendHeartbeat(IoSession currSession) {
       
       try {
          if(currSession.isHeartbeated()) {
             return false;
          }
          ImMsg.Builder imMsgBuilder = ImMsg.newBuilder();
          
          currSession.sendMessage(imMsgBuilder.build());
          
          currSession.setHeartbeated(true);
          
          return true;
       }catch(Exception e) {
          log.error("主動發送心跳包時發生異常:", e);
          currSession.close(SessionCloseReason.EXCEPTION);
          return false;
       }
       
    }
    /**
     * 返回客戶端發來的心跳包消息
     * @param imMsg
     * @param currSession
     */
    private void responseHeartbeat(ImMsg imMsg,IoSession currSession) {
       ImMsg.Builder imMsgBuilder = ImMsg.newBuilder();
       
       currSession.sendMessage(imMsgBuilder.build());
    }
    
}

最後到service業務處理TerminalService

package com.chimeta.netty.service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chimeta.common.entity.terminal.TerminalStateMonitorDO;
import com.chimeta.netty.IoSession;
import com.chimeta.netty.constant.MessagingConst;
import com.chimeta.netty.model.DeviceInfo;
import com.chimeta.netty.protobuf.ImProto;
import com.chimeta.netty.util.MessageBuilder;
import com.chimeta.terminal.mapper.TerminalStateMonitorMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * 盒子設備相關的實現類
 */
@Service
@Slf4j
public class TerminalService extends ServiceImpl<TerminalStateMonitorMapper, TerminalStateMonitorDO> {

    @Transactional(rollbackFor = Exception.class)
    public void multiInsert(ImProto.ImMsg imMsg, IoSession currSession){
        DeviceInfo deviceInfo = currSession.getDevice();
        if(deviceInfo == null) {
            currSession.sendMessage(MessageBuilder.buildErrorResponse(imMsg, MessagingConst.RESPONSE_ERR_CODE_400, "device not online!"));
            return;
        }
        try {
            ImProto.TerminalStateList terminalStateList = imMsg.getMsg().unpack(ImProto.TerminalStateList.class);
            log.info("TerminalService multiInsert TerminalStateList:{}", terminalStateList);
            List<ImProto.TerminalState> requestTerminalStateList = terminalStateList.getTerminalStateList();

            if (!CollectionUtils.isEmpty(requestTerminalStateList)){
                List<TerminalStateMonitorDO> tmplist = new ArrayList<>();
                for (ImProto.TerminalState requestTerminalState : requestTerminalStateList){
                    TerminalStateMonitorDO terminalStateMonitorDO = new TerminalStateMonitorDO();
                    terminalStateMonitorDO.setBatteryLevel(requestTerminalState.getBatteryLevel());
                    terminalStateMonitorDO.setChargingState(requestTerminalState.getChargingState());
                    terminalStateMonitorDO.setTemperature(BigDecimal.valueOf(requestTerminalState.getTemperature()));
                    terminalStateMonitorDO.setUniqueCode(deviceInfo.getDeviceCode());
                    terminalStateMonitorDO.setStateTime(requestTerminalState.getStateTime());
                    tmplist.add(terminalStateMonitorDO);
                }
                this.saveBatch(tmplist);
            }
        } catch (Exception e) {
            log.error("TerminalService multiInsert error:{}", e);
        }

    }

}

至此,服務端的處理邏輯寫完,然後比較費時間的是自己寫client的請求,終於經過兩三天時間總結好了,寫了個test類,如下

package com.chimeta.core;

import com.chimeta.netty.protobuf.ImProto;
import com.google.protobuf.Any;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;


@Slf4j
@RunWith(MockitoJUnitRunner.class)
class NettyTerminalTest {


    @Test
    public void tryTest()  throws InterruptedException {

        ImProto.TerminalStateList terminalstateList = ImProto.TerminalStateList.newBuilder().build();
        for (int i = 0; i < 3; i++) {
            ImProto.TerminalState build = ImProto.TerminalState.newBuilder()
                    .setBatteryLevel(i)
                    .setChargingState(i * 11)
                    .setTemperature(i * 11.1)
                    .setStateTime(i * 111)
                    .build();
            terminalstateList = terminalstateList.toBuilder().addTerminalState(build).build();
        }

        ImProto.ImMsg imMsg = ImProto.ImMsg.newBuilder().setId(66).setMsg(Any.pack(terminalstateList)).build();

        Channel channel = new Bootstrap()
                .group(new NioEventLoopGroup(1))
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        System.out.println("初始化連接...");
                        ch.pipeline().addLast("encode", new ProtobufEncoder())
                                .addLast(new ProtobufVarint32FrameDecoder()).addLast(new ProtobufVarint32LengthFieldPrepender());
                    }
                })
                .channel(NioSocketChannel.class).connect("192.168.123.170", 10001)
                .sync()
                .channel();

//        channel.pipeline().addLast(new StringEncoder()).writeAndFlush(ByteBufAllocator.DEFAULT.buffer().writeBytes(imMsg.toByteArray()));
        channel.pipeline().writeAndFlush(Unpooled.copiedBuffer(imMsg.toByteArray()));
        System.out.println("over!");
    }

}

 好了,記錄下,以後就不會忘記了

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 寫在前面 tips:點贊 + 收藏 = 學會! 我們已經介紹了radash的相關信息和部分Array相關方法,詳情可前往主頁查看。 本篇我們繼續介紹radash中Array的相關方法的剩餘方法。 本期文章發佈後,作者也會同步整理出Array方法的使用目錄,包括文章說明和腦圖說明。 因為方法較多,後續 ...
  • 前言 還是上一篇面試官:來說說vue3是怎麼處理內置的v-for、v-model等指令? 文章的那個粉絲,面試官接著問了他另外一個v-model的問題。 面試官:vue3的v-model都用過吧,來講講。 粉絲:v-model其實就是一個語法糖,在編譯時v-model會被編譯成:modelValue ...
  • 前言 大家在開發過程中,或多或少都會用到輪播圖之類的組件,PC和Mobile上使用 Swiper.js ,小程式上使用swiper組件等。 本文將詳細講解如何用Vue一步步實現的類似Swiper.js的功能,無任何第三方依賴,乾貨滿滿。 最終效果 線上預覽:https://zyronon.githu ...
  • 本篇作為《Vue+OpenLayers6入門教程》和《Vue+OpenLayers6實戰進階案例》所有文章的二合一彙總目錄,方便查找。 本專欄源碼是由OpenLayers6.15.1版本結合Vue2框架編寫,同時支持Vue3,零星幾篇文章用到了Element-UI庫。 本專欄從Vue搭建腳手架到如何 ...
  • 一、題目及運行環境 1.小組成員 2252331 與 2252336 2.題目 小學老師要每周給同學出300道四則運算練習題。 這個程式有很多種實現方式: C/C++ C#/VB.net/Java Excel Unix Shell Emacs/Powershell/Vbscript Perl Pyt ...
  • 重載(Overloading):所謂重載是指不同的函數實體共用一個函數名稱。例如以下代碼所提到的CPoint之中,有兩個member functions的名稱同為x(): 1 class CPoint{ 2 3 public: 4 float x(); 5 void x(float xval); 6 ...
  • 實驗要求一:對比分析 對比分析墨刀、Axure、Mockplus等原型設計工具的各自的適用領域及優缺點。 一丶墨刀 墨刀是一款線上的產品設計協作軟體,可以解決產設研團隊中存在的項目管理許可權不明、版本管理混亂、協作低效等諸多問題。 優點: 功能強大:可滿足產品經理、設計師、開發在產品設計和團隊協作上的 ...
  • title: 文本語音互相轉換系統設計 date: 2024/4/24 21:26:15 updated: 2024/4/24 21:26:15 tags: 需求分析 模塊化設計 性能優化 系統安全 智能化 跨平臺 區塊鏈 第一部分:導論 第一章:背景與意義 文本語音互相轉換系統的定義與作用 文本語 ...
一周排行
    -Advertisement-
    Play Games
  • 基於.NET Framework 4.8 開發的深度學習模型部署測試平臺,提供了YOLO框架的主流系列模型,包括YOLOv8~v9,以及其系列下的Det、Seg、Pose、Obb、Cls等應用場景,同時支持圖像與視頻檢測。模型部署引擎使用的是OpenVINO™、TensorRT、ONNX runti... ...
  • 十年沉澱,重啟開發之路 十年前,我沉浸在開發的海洋中,每日與代碼為伍,與演算法共舞。那時的我,滿懷激情,對技術的追求近乎狂熱。然而,隨著歲月的流逝,生活的忙碌逐漸占據了我的大部分時間,讓我無暇顧及技術的沉澱與積累。 十年間,我經歷了職業生涯的起伏和變遷。從初出茅廬的菜鳥到逐漸嶄露頭角的開發者,我見證了 ...
  • C# 是一種簡單、現代、面向對象和類型安全的編程語言。.NET 是由 Microsoft 創建的開發平臺,平臺包含了語言規範、工具、運行,支持開發各種應用,如Web、移動、桌面等。.NET框架有多個實現,如.NET Framework、.NET Core(及後續的.NET 5+版本),以及社區版本M... ...
  • 前言 本文介紹瞭如何使用三菱提供的MX Component插件實現對三菱PLC軟元件數據的讀寫,記錄了使用電腦模擬,模擬PLC,直至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1. PLC開發編程環境GX Works2,GX Works2下載鏈接 https:// ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • 1、jQuery介紹 jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝 ...
  • 前言 之前的文章把js引擎(aardio封裝庫) 微軟開源的js引擎(ChakraCore))寫好了,這篇文章整點js代碼來測一下bug。測試網站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻譯js逆向(MD5加密,AES加密)附完整源碼 ...
  • 引言 現代的操作系統(Windows,Linux,Mac OS)等都可以同時打開多個軟體(任務),這些軟體在我們的感知上是同時運行的,例如我們可以一邊瀏覽網頁,一邊聽音樂。而CPU執行代碼同一時間只能執行一條,但即使我們的電腦是單核CPU也可以同時運行多個任務,如下圖所示,這是因為我們的 CPU 的 ...
  • 掌握使用Python進行文本英文統計的基本方法,並瞭解如何進一步優化和擴展這些方法,以應對更複雜的文本分析任務。 ...
  • 背景 Redis多數據源常見的場景: 分區數據處理:當數據量增長時,單個Redis實例可能無法處理所有的數據。通過使用多個Redis數據源,可以將數據分區存儲在不同的實例中,使得數據處理更加高效。 多租戶應用程式:對於多租戶應用程式,每個租戶可以擁有自己的Redis數據源,以確保數據隔離和安全性。 ...