基于python3封装的redis stream操作

基于python3封装的redis stream操作。
主要有两块

  • 基于redis封装的RedisService用来获取redis客户端

    • 实现连接池
    • 实现直连
  • 基于哨兵封装的SentinelService继承自RedisService

    • 重写获取连接池,增加连接池缓存(master不变使用缓存)
    • 重写直连(根据哨兵解析出master节点)
  • 基于常用的stream命令的封装RedisStreamService

第三个,可以自己扩展封装其他的命令

看下类图:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time    : 2022/4/02 7:35 下午
# @FileName: redisService.py

import redis,logging
from redis.client import *
from redis.sentinel import Sentinel
import json

class RedisService(object):
    """
    直接操作redis
    """
    def __init__(self,host=None,port=None,pwd=None,db=0,max_connections=10):
        """
        redis链接初始化
        :param host: redis实例的地址
        :param port:  端口
        :param pwd: 密码
        :param db:  使用的db
        :param max_connections: 连接池大小
        """
        self.redis_pool = None
        self.host = host
        self.port = port
        self.pwd = pwd
        self.db = db
        self.max_connections = max_connections

    def __getPool(self,flag):
        """
        获取连接池(主从切换以后把flag设置为True,重新初始化连接池)
        :param flag: 为False不用重新获取,为True需要重新获取
        :param max_connections: 连接池大小
        :return:
        """
        try:
            if not flag:
                return self.pool
            # 缓存连接池   
            self.pool =  redis.ConnectionPool(host=self.host, port=self.port, db=self.db, password=self.pwd, max_connections=self.max_connections)
            return self.pool
        except Exception as e:
            logging.error(e)
            raise e
    def getRedisFromPool(self,flag = False) -> Redis:
        """
        从链接池获取redis链接
        :param flag: 是否重新获取(主从切换后)
        :return:
        """
        return redis.Redis(connection_pool=self.__getPool(flag))
    def getRedis(self) -> Redis:
        """
        直接获取redis链接
        :return:
        """
        if self.pwd:
            return redis.Redis(host=self.host, port=self.port, db=self.db, password=self.pwd)
        return redis.Redis(host=self.host, port=self.port, db=self.db)


class SentinelService(RedisService):
    """
    通过哨兵操作redis
    """
    def __init__(self,sentinel_nodes_str=None,service_name=None,pwd = None,max_connections=10):
        """
        初始化哨兵服务
        :param sentinel_nodes_str: 哨兵地址
        :param service_name: 集群服务名称
        :param pwd:  密码
        :param max_connections:连接池大小
        """
        super(SentinelService, self).__init__(pwd = pwd,max_connections=max_connections)
        #缓存的master的地址(如果一台机器不同端口,请拼接)
        self.master_host = None
        self.sentinel_nodes_str = sentinel_nodes_str
        self.service_name = service_name
        self.sentinel_nodes = self.__parseNodesStr()

    def __parseNodesStr(self):
        """
        哨兵参数解析
        :return:
        """
        try:
            list = []
            for sentinelInfo in self.sentinel_nodes_str.split(','):
                kv = sentinelInfo.split(":")
                list.append((kv[0], kv[1]))
            logging.info("from sentinel: %s", list)
            return list
        except Exception as e:
            logging.error(e)
            raise e

    def __getMaster(self):
        """
        从哨兵节点获取master节点
        :return:
        """
        # 链接哨兵节点
        sentinel = Sentinel(self.sentinel_nodes, socket_timeout=0.1)
        # 获取master节点
        master = sentinel.discover_master(self.service_name)
        if master:
            # 赋值给RedisService
            self.host = master[0]
            self.port = master[1]
        return master

    def getRedisFromPool(self, flag=False) -> Redis:
        """
        从链接池获取redis客户端
        :param flag:
        :return:
        """
        # 初始化master节点
        master = self.__getMaster()
        # 没有主从切换,直接从连接池里获取
        if self.master_host in master:
            return super().getRedisFromPool()
        # 主从切换了,更新缓存
        self.master_host = master[0]
        return super().getRedisFromPool(True)
    def getRedis(self) -> Redis:
        """
        直接获取redis客户端
        :return:
        """
        self.__getMaster()
        return super().getRedis()

class RedisStreamService(object):
    """
    redis stream封装
    """
    def __init__(self,redis,stream_name,consumer_group):
        """
        :param redis:redis的客户端
        :param stream_name:stream的名称
        :param consumer_group:消费组名称
        """
        self.redis = redis
        self.stream_name = stream_name
        self.consumer_group = consumer_group

    def __streamInit(self,data,id="0",maxlen=20000,target = None):
        """
        初始化stream,上线之前手动调用下即可,不用在项目里调用
        :param data: 业务数据  测试数据
        :param id: 0 从开始消费, $ 从创建以后新进来的开始消费
        :param maxlen: 队里最大长度
        :return:
        """
        if not self.redis.exists(self.stream_name):
            """
            不存在消费者,直接创建消费者和消费组
            """
            self.redis.xadd(self.stream_name, self.__dataWrap(data), maxlen=maxlen)
            self.xgroup_create(id)
            self.redis.xadd(self.stream_name, self.__dataWrap(data), maxlen=maxlen)
        self.consumer("robot",count=2,target=target)
    def xgroup_create(self,id):
        """
        创建消费组
        :param id:
        :return:
        """
        self.redis.xgroup_create(self.stream_name, self.consumer_group, id=id)
    def __dataWrap(self,data) -> dict:
        """
        包装数据
        :param data:
        :return:
        """
        return  {"bizData":json.dumps(data)}
    def xack(self,msgId):
        """
        ack
        :param msgId:
        :return:
        """
        self.redis.xack( self.stream_name,  self.consumer_group, msgId)

    def __getBizData(self,data):
        """
        从消息流中获取业务数据
        :param item:
        :return:
        """
        if not data or not data[0]:
            return None, None
        msgId = str(data[0], 'utf-8')
        data = {str(key, 'utf-8'): str(val, 'utf-8') for key, val in data[1].items()}
        return msgId, data["bizData"]
    def add(self,data):
        """
        新增数据
        :param stream_name:
        :param data:
        :return:
        """
        self.redis.xadd( self.stream_name, self.__dataWrap(data))
    def consumer(self,consumer_name,id=">",block=60000, count=1,target=None):
        """
        消费数据
        :param consumer_name: 消费者名称,建议传递ip
        :param id: 从哪开始消费
        :param block: 无消息阻塞时间,毫秒,默认60秒,在60秒内有消息直接消费
        :param count: 消费多少条,默认1
        :param target: 业务处理回调方法
        :return:
        """
        # block 0 时阻塞等待, 其他数值表示读取超时时间
        streams = {self.stream_name: id}
        rst = self.redis.xreadgroup( self.consumer_group, consumer_name, streams, block=block, count=count)
        print(f'消费到的数据 {rst}')
        if not rst or not rst[0] or not rst[0][1]:
           return None
        # 遍历获取到的列表信息(可以消费多条,根据count)
        for item in rst[0][1]:
           try:
               #解析数据
               msgId, data = self.__getBizData(item)
               """
               执行回调函数target,成功后ack
               """
               if target and target(msgId,data):
                   # 将处理完成的消息标记,类似于kafka的offset
                   self.redis.xack( self.stream_name,  self.consumer_group, msgId)
           except Exception as e:
               # 消费失败,下次从头消费(消费成功的都已经提交ack了,可以先不处理,以后再处理)
               logging.error("consumer is error:",e)

真实使用场景

def biz_execute(msgId,data):
    """
    业务处理,不建议多个场景共用一个stream,建议分开,
    如果数据量比较少,通过工厂处理分发
    :param msgId:
    :param data:
    :return:
    """
    print(f'业务执行msgId={msgId} bizData={data}')
    return True

host = "127.0.0.1"
port = 6379
pwd = "123456"
#集群名称
service_name = "sentinel-test"
#哨兵节点
sentinel_nodes_str = "10.21.41.1:6381,10.21.41.2:6382,10.21.41.3:6381"
# stream对应的key
stream_name = "stream:bizName"
# 消费组
consumer_group = "biz_group"
def subscribe():
    #基于redis的
    service = RedisService(host=host, port=port, pwd=pwd)
    #基于哨兵的
    # service = SentinelService(sentinel_nodes_str, service_name, pwd=pwd)
    while True:
        r = service.getRedisFromPool()
        stream = RedisStreamService(r, stream_name, consumer_group)
        stream.consumer("当前ip",target=biz_execute)

#起一个后台线程执行消费,防止阻塞主线程
from threading import *
t1 = Thread(target=subscribe)
t1.start()


#测试代码(测试和后台线程二选一)
def test():
    service = RedisService(host=host,port=port,pwd=pwd)
    r = service.getRedisFromPool()
    stream =  RedisStreamService(r,stream_name,consumer_group)
    data = {"key1":"init@init","key2":"val2"}
    stream.__streamInit(data,id="0",maxlen=10,target=biz_execute)
    # service = SentinelService(sentinel_nodes_str,service_name,pwd = pwd)
    # r = service.getRedisFromPool()
if __name__ == '__main__':
    test()

输出

 python3 redisService.py
消费到的数据 [[b'stream:bizName', [(b'1648806192012-0', {b'bizData': b'{"key1": "init@init", "key2": "val2"}'}), (b'1648806192012-1', {b'bizData': b'{"key1": "xxxxy2": "val2"}'})]]]
业务执行msgId=1648806192012-0 bizData={"key1": "init@init", "key2": "val2"}
业务执行msgId=1648806192012-1 bizData={"key1": "xxxx", "key2": "val2"}

<think>好的,用户之前问过如何将人脸识别登录集成到若依框架的前端,使用Vue3、TS和Element Plus,并且要求使用免费开源的第三方插件。我之前给出的回答包括了前端使用face-api.js,后端用Python微服务的方案。但现在用户明确说明后端不用Python,而是使用Spring Boot和MyBatis Plus,特别是基于若依的后端框架。我需要调整之前的回答,重点放在如何用Java后端实现人脸识别功能。 首先,我需要确认若依的后端框架是否支持或需要哪些调整。若依是基于Spring Boot的,所以用户希望在后端用Java处理人脸识别逻辑。但Java生态中的人脸识别库相对较少,可能需要用JavaCV或者调用本地库如OpenCV。此外,用户之前提到若依的后端框架,可能需要整合现有的用户认证体系,比如结合Spring Security或者若依自带的权限管理。 接下来,技术选型方面,前端还是用face-api.js,因为用户已经接受了这个方案。后端方面,需要找到适合Java的开源库。JavaCV是一个不错的选择,它封装了OpenCV,可以在Java中使用。另外,可能需要加载模型文件,进行人脸检测和特征提取。 然后,我需要考虑如何设计API接口。注册和登录的流程需要定义对应的接口,比如注册时上传人脸特征向量,登录时发送当前捕获的特征向量进行比对。数据库方面,用户表可能需要新增一个字段来存储人脸特征向量,这里需要注意数据类型,比如BLOB或者将浮点数组转换为二进制存储。 安全性方面,用户提到要使用HTTPS和加密存储特征值,这可能需要在代码中实现AES加密,或者使用数据库的加密功能。此外,活体检测可能需要前端配合,比如让用户眨眼或转头,但Java后端如何验证这一点呢?可能需要前端在捕获时进行活体检测,或者后端通过分析连续帧来判断。 然后,我需要编写具体的代码示例。比如,如何用JavaCV进行人脸检测和特征提取,如何将特征向量存储到数据库,如何实现比对逻辑。可能需要加载OpenCV的库文件,处理图像,提取特征,然后计算相似度。需要注意的是,特征向量通常是128维的浮点数组,比对时计算欧氏距离或余弦相似度。 另外,若依框架本身有用户管理和权限模块,如何将人脸识别集成到现有的登录流程中?可能需要新增一个登录方式,比如在登录页面添加人脸识别的选项,调用摄像头捕获图像,发送到后端验证,验证通过后生成Token或Session,完成登录。 还需要考虑性能优化,比如使用缓存(Redis)存储特征向量,加快比对速度。同时,Java处理图像可能比较耗资源,需要优化代码,可能用异步处理或多线程。 最后,测试方面,需要确保不同浏览器和设备的兼容性,处理摄像头权限问题,以及后端服务的稳定性。同时,用户提到的安全措施,如加密和活体检测,可能需要进一步细化实现步骤。 总结一下,我需要调整之前的后端方案,用JavaCV和OpenCV实现人脸识别,整合到若依的Spring Boot后端,设计对应的接口和数据库表,处理特征存储和比对,同时确保安全和性能。---</think> ### **后端实现(基于 Spring Boot + 若依框架)** 若依框架本身基于 Spring Boot + MyBatis Plus,需结合 **JavaCV**(OpenCV 的 Java 封装)实现人脸特征比对。以下是关键代码实现步骤: --- ### **一、技术选型** 1. **JavaCV + OpenCV** - **功能**:通过 Java 调用 OpenCV 实现人脸检测、特征提取和比对。 - **依赖**:需引入 OpenCV 本地库(`.dll`/`.so`)并加载。 - **模型文件**:使用 OpenCV 预训练的 `haarcascade_frontalface_alt.xml`(人脸检测模型)。 2. **若依框架集成** - 复用原有用户体系(`sys_user` 表),新增字段存储人脸特征值。 --- ### **二、数据库调整** 在 `sys_user` 表中新增字段 `face_feature`,用于存储人脸特征向量(二进制或 JSON 化数组): ```sql ALTER TABLE sys_user ADD COLUMN face_feature LONGBLOB COMMENT &#39;人脸特征向量&#39;; ``` --- ### **三、核心代码实现** #### **1. 依赖引入(pom.xml)** ```xml <!-- OpenCV 本地库支持 --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.5.7</version> </dependency> ``` #### **2. 加载 OpenCV 本地库** 在 Spring Boot 启动时加载 OpenCV 动态库(需提前将库文件放入 `resources/lib`): ```java @PostConstruct public void initOpenCV() { File libFile = new ClassPathResource("lib/opencv_java460.dll").getFile(); System.load(libFile.getAbsolutePath()); } ``` #### **3. 人脸注册接口** ```java @PostMapping("/face/register") public AjaxResult registerFace(@RequestParam("userId") Long userId, @RequestParam("file") MultipartFile file) { try { // 1. 检测人脸并提取特征 Mat faceFeature = extractFaceFeature(file.getBytes()); // 2. 保存特征到数据库(序列化为字节数组) SysUser user = userService.selectUserById(userId); user.setFaceFeature(matToByteArray(faceFeature)); userService.updateUser(user); return AjaxResult.success("人脸注册成功"); } catch (Exception e) { return AjaxResult.error("人脸注册失败: " + e.getMessage()); } } // 提取人脸特征(示例:使用 OpenCV 检测人脸并生成特征向量) private Mat extractFaceFeature(byte[] imageData) { Mat img = Imgcodecs.imdecode(new MatOfByte(imageData), Imgcodecs.IMREAD_COLOR); CascadeClassifier faceDetector = new CascadeClassifier("haarcascade_frontalface_alt.xml"); MatOfRect faceDetections = new MatOfRect(); faceDetector.detectMultiScale(img, faceDetections); if (faceDetections.empty()) { throw new RuntimeException("未检测到人脸"); } // 简化示例:实际需替换为特征提取算法(如 LBPH 或深度学习模型) Mat faceFeature = new Mat(); Imgproc.resize(img.submat(faceDetections.toArray()[0]), faceFeature, new Size(128, 128)); return faceFeature; } // 将 Mat 转换为字节数组(存储到数据库) private byte[] matToByteArray(Mat mat) { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Imgcodecs.imencode(".png", mat, byteStream); return byteStream.toByteArray(); } ``` #### **4. 人脸登录接口** ```java @PostMapping("/face/login") public AjaxResult faceLogin(@RequestParam("file") MultipartFile file) { try { // 1. 提取登录特征 Mat loginFeature = extractFaceFeature(file.getBytes()); // 2. 从数据库获取所有用户特征进行比对 List<SysUser> users = userService.selectUserList(new SysUser()); for (SysUser user : users) { Mat savedFeature = byteArrayToMat(user.getFaceFeature()); double similarity = compareFeatures(loginFeature, savedFeature); if (similarity > 0.8) { // 阈值可调整 // 生成 Token 并返回登录成功 String token = loginService.login(user); return AjaxResult.success("登录成功").put("token", token); } } return AjaxResult.error("人脸不匹配"); } catch (Exception e) { return AjaxResult.error("登录失败: " + e.getMessage()); } } // 特征比对(示例:计算图像直方图相似度) private double compareFeatures(Mat feature1, Mat feature2) { Mat hist1 = new Mat(), hist2 = new Mat(); Imgproc.calcHist(Arrays.asList(feature1), new MatOfInt(0), new Mat(), hist1, new MatOfInt(256), new MatOfFloat(0, 256)); Imgproc.calcHist(Arrays.asList(feature2), new MatOfInt(0), new Mat(), hist2, new MatOfInt(256), new MatOfFloat(0, 256)); return Imgproc.compareHist(hist1, hist2, Imgproc.CV_COMP_CORREL); } // 从数据库字节数组恢复 Mat private Mat byteArrayToMat(byte[] bytes) { return Imgcodecs.imdecode(new MatOfByte(bytes), Imgcodecs.IMREAD_COLOR); } ``` --- ### **四、安全优化** 1. **特征加密存储** ```java // 使用 AES 加密特征数据 public void setFaceFeature(byte[] feature) { this.faceFeature = AESUtils.encrypt(feature, SECRET_KEY); } ``` 2. **活体检测(简易版)** 前端通过连续采集多帧,检测人脸移动或眨眼动作(需结合 `face-api.js` 的 `withFaceExpressions()`)。 --- ### **五、部署注意事项** 1. **OpenCV 库文件** - 将 `opencv_java460.dll`(Windows)或 `libopencv_java460.so`(Linux)放入 `src/main/resources/lib`。 - 在启动脚本中添加 `-Djava.library.path=classpath:lib/`。 2. **性能优化** - 使用 Redis 缓存用户特征数据,减少数据库查询。 - 特征比对算法替换为高效模型(如 **FaceNet** 的 ONNX 格式,通过 Java 推理)。 --- ### **六、代码扩展性建议** 1. **特征算法升级** 替换 `extractFaceFeature()` 方法为深度学习模型(如 **ArcFace**),需将模型转换为 ONNX 格式,并通过 Java 推理框架(如 **DeepJavaLibrary**)加载。 2. **异步处理** 使用 Spring 异步任务(`@Async`)处理人脸比对,避免阻塞主线程。 --- 通过以上方案,可在若依的 Spring Boot 后端中实现人脸识别登录,并与原有用户体系无缝集成。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值