利用redis实现聊天记录转存功能的全过程

目录
  • 前言
  • 环境搭建
  • 实现思路
  • 实现过程
    • 自定义redistemplate
    • 封装redis工具类
    • 进行单元测试
      • 测试list数据的写入与获取
      • 测试list数据的取出
      • 测试聊天记录转移至数据库
    • 解析客户端数据保存至redis
      • 定时将redis的数据写入mysql
      • 实现效果
        • 总结

          前言

          前一阵子实现了我开源项目的单聊功能,在实现过程中遇到了需要将聊天记录保存至数据库的问题,在收到消息时肯定不能直接存数据库,因为这样在高并发的场景下,数据库就炸了。

          于是,我就想到了redis这个东西,第一次听说它是在2年前,但是一直没时间玩他,现在终于遇到了需要使用它的场景,在用的时候学它,本文就跟大家分享下我的实现思路以及过程,欢迎各位感兴趣的开发者阅读本文。

          环境搭建

          我的项目是基于springboot2.x搭建的,电脑已经安装了redis,用的maven作为jar包管理工具,所以只需要在maven中添加需要的依赖包即可,如果你用的是其他管理工具,请自行查阅如何添加依赖。

          <!-- redis -->
          <dependency>
              <groupid>org.springframework.boot</groupid>
              <artifactid>spring-boot-starter-data-redis</artifactid>
          </dependency>
          <!-- 定时任务调度 -->
          <dependency>
              <groupid>org.springframework.boot</groupid>
              <artifactid>spring-boot-starter-quartz</artifactid>
              <version>2.3.7.release</version>
          </dependency>
          

          本文需要用到依赖:redis 、quartz,在pom.xml文件的dependencies标签下添加下述代码。

          spring:
          # redis配置
            redis:
              host: 127.0.0.1 # redis地址
              port: 6379 # 端口号
              password:  # 密码
              timeout: 3000 # 连接超时时间,单位毫秒
          

          实现思路

          在websocket的服务中,收到客户端推送的消息后,我们对数据进行解析,构造聊天记录实体类,将其保存至redis中,最后我们使用quartz设置定时任务将redis的数据定时写入mysql中。

          我们将上述思路进行下整理:

          1. 解析客户端数据,构造实体类
          2. 将数据保存至redis
          3. 使用quartz将redis中的数据定时写入mysql

          实现过程

          实现思路很简单,难在如何将实体类数据保存至redis,我们需要把redis这一块配置好后,才能继续实现我们的业务需求。

          redis支持的数据结构类型有:

          • set 集合,string类型的无序集合,元素不允许重复
          • hash 哈希表,键值对的集合,用于存储对象
          • list 列表,链表结构
          • zset有序集合
          • string 字符串,最基本的数据类型,可以包含任何数据,比如一个序列化的对象,它的字符串大小上限是512mb

          redis的客户端分为jedis 和 lettuce,在springboot2.x中默认客户端是使用lettuce实现的,因此我们不用做过多配置,在使用的时候通过redistemplate.xxx来对redis进行操作即可。

          自定义redistemplate

          在redistemplate中,默认是使用java字符串序列化,将字符串存入redis后可读性很差,因此,我们需要对他进行自定义,使用jackson 序列化,以 json 方式进行存储。

          我们在项目的config包下,创建一个名为lettuceredisconfig的java文件,我们再此文件中配置其默认序列化规则,它的代码如下:

          package com.lk.config;
          
          import org.springframework.context.annotation.bean;
          import org.springframework.context.annotation.configuration;
          import org.springframework.data.redis.connection.redisconnectionfactory;
          import org.springframework.data.redis.core.redistemplate;
          import org.springframework.data.redis.serializer.genericjackson2jsonredisserializer;
          import org.springframework.data.redis.serializer.stringredisserializer;
          
          
          // 自定义redistemplate设置序列化器, 方便转换redis中的数据与实体类互转
          @configuration
          public class lettuceredisconfig {
              /**
               * redis 序列化配置
               */
              @bean
              public redistemplate<string, object> redistemplate(redisconnectionfactory connectionfactory) {
                  redistemplate<string, object> redistemplate = new redistemplate<>();
                  redistemplate.setconnectionfactory(connectionfactory);
                  // 使用genericjackson2jsonredisserializer替换默认序列化
                  genericjackson2jsonredisserializer jackson2jsonredisserializer = new genericjackson2jsonredisserializer();
                  // 设置 key 和 value 的序列化规则
                  redistemplate.setkeyserializer(new stringredisserializer());
                  redistemplate.setvalueserializer(jackson2jsonredisserializer);
                  redistemplate.sethashkeyserializer(new stringredisserializer());
                  redistemplate.sethashvalueserializer(jackson2jsonredisserializer);
                  // 初始化 redistemplate 序列化完成
                  redistemplate.afterpropertiesset();
                  return redistemplate;
              }
          }
          
          

          封装redis工具类

          做完上述操作后,通过redistemplate存储到redis中的数据就是json形式的了,接下来我们对其常用的操作封装成工具类,方便我们在项目中使用。

          在utils包中创建一个名为redisoperatingutil,其代码如下:

          package com.lk.utils;
          
          import org.springframework.data.redis.connection.datatype;
          import org.springframework.data.redis.core.redistemplate;
          import org.springframework.stereotype.component;
          
          import javax.annotation.resource;
          import java.util.arrays;
          import java.util.collections;
          import java.util.list;
          import java.util.map;
          import java.util.concurrent.timeunit;
          
          @component
          // redis操作工具类
          public class redisoperatingutil {
              @resource
              private redistemplate<object, object> redistemplate;
          
              /**
               * 指定 key 的过期时间
               *
               * @param key  键
               * @param time 时间(秒)
               */
              public void setkeytime(string key, long time) {
                  redistemplate.expire(key, time, timeunit.seconds);
              }
          
              /**
               * 根据 key 获取过期时间(-1 即为永不过期)
               *
               * @param key 键
               * @return 过期时间
               */
              public long getkeytime(string key) {
                  return redistemplate.getexpire(key, timeunit.seconds);
              }
          
              /**
               * 判断 key 是否存在
               *
               * @param key 键
               * @return 如果存在 key 则返回 true,否则返回 false
               */
              public boolean haskey(string key) {
                  return redistemplate.haskey(key);
              }
          
              /**
               * 删除 key
               *
               * @param key 键
               */
              public long delkey(string... key) {
                  if (key == null || key.length < 1) {
                      return 0l;
                  }
                  return redistemplate.delete(arrays.aslist(key));
              }
          
              /**
               * 获取 key 的类型
               *
               * @param key 键
               */
              public string keytype(string key) {
                  datatype datatype = redistemplate.type(key);
                  assert datatype != null;
                  return datatype.code();
              }
          
              /**
               * 批量设置值
               *
               * @param map 要插入的 key value 集合
               */
              public void barchset(map<string, object> map) {
                  redistemplate.opsforvalue().multiset(map);
              }
          
              /**
               * 批量获取值
               *
               * @param list 查询的 key 列表
               * @return value 列表
               */
              public list<object> batchget(list<string> list) {
                  return redistemplate.opsforvalue().multiget(collections.singleton(list));
              }
          
          
              /**
               * 获取指定对象类型key的值
               *
               * @param key 键
               * @return 值
               */
              public object objectgetkey(string key) {
                  return redistemplate.opsforvalue().get(key);
              }
          
              /**
               * 设置对象类型的数据
               *
               * @param key   键
               * @param value 值
               */
              public void objectsetvalue(string key, object value) {
                  redistemplate.opsforvalue().set(key, value);
              }
          
              /**
               * 向list的头部插入一条数据
               *
               * @param key   键
               * @param value 值
               */
              public long listleftpush(string key, object value) {
                  return redistemplate.opsforlist().leftpush(key, value);
              }
          
              /**
               * 向list的末尾插入一条数据
               *
               * @param key   键
               * @param value 值
               */
              public long listrightpush(string key, object value) {
                  return redistemplate.opsforlist().rightpush(key, value);
              }
          
              /**
               * 向list头部添加list数据
               *
               * @param key   键
               * @param value 值
               */
              public long listleftpushall(string key, list<object> value) {
                  return redistemplate.opsforlist().leftpushall(key, value);
              }
          
              /**
               * 向list末尾添加list数据
               *
               * @param key   键
               * @param value 值
               */
              public long listrightpushall(string key, list<object> value) {
                  return redistemplate.opsforlist().rightpushall(key, value);
              }
          
              /**
               * 通过索引设置list元素的值
               *
               * @param key   键
               * @param index 索引
               * @param value 值
               */
              public void listindexset(string key, long index, object value) {
                  redistemplate.opsforlist().set(key, index, value);
              }
          
              /**
               * 获取列表指定范围内的list元素,正数则表示正向查找,负数则倒叙查找
               *
               * @param key   键
               * @param start 开始
               * @param end   结束
               * @return boolean
               */
              public object listrange(string key, long start, long end) {
                  return redistemplate.opsforlist().range(key, start, end);
              }
          
              /**
               * 从列表前端开始取出数据
               *
               * @param key 键
               * @return 结果数组对象
               */
              public object listpopleftkey(string key) {
                  return redistemplate.opsforlist().leftpop(key);
              }
          
              /**
               * 从列表末尾开始遍历取出数据
               *
               * @param key 键
               * @return 结果数组
               */
              public object listpoprightkey(string key) {
                  return redistemplate.opsforlist().rightpop(key);
              }
          
              /**
               * 获取list长度
               *
               * @param key 键
               * @return 列表长度
               */
              public long listlen(string key) {
                  return redistemplate.opsforlist().size(key);
              }
          
              /**
               * 通过索引获取list中的元素
               *
               * @param key   键
               * @param index 索引(index>=0时,0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推)
               * @return 列表中的元素
               */
              public object listindex(string key, long index) {
                  return redistemplate.opsforlist().index(key, index);
              }
          
              /**
               * 移除list元素
               *
               * @param key   键
               * @param count 移除数量("负数"则从列表倒叙查找删除 count 个对应的值; "整数"则从列表正序查找删除 count 个对应的值;)
               * @param value 值
               * @return 成功移除的个数
               */
              public long listrem(string key, long count, object value) {
                  return redistemplate.opsforlist().remove(key, count, value);
              }
          
              /**
               * 截取指定范围内的数据, 移除不是范围内的数据
               * @param key 操作的key
               * @param start 截取开始位置
               * @param end 截取激素位置
               */
              public void listtrim(string key, long start, long end) {
                  redistemplate.opsforlist().trim(key, start, end);
              }
          }
          
          

          进行单元测试

          做完上述操作后,最难弄的一关我们就已经搞定了,接下来我们来对一会需要使用的方法进行单元测试,确保其能够正常运行。

          创建一个名为redistest的java文件,注入需要用到的相关类。

          • redisoperatingutil为我们的redis工具类
          • submessagemapper为聊天记录表的dao层
          @runwith(springrunner.class)
          @springboottest
          @slf4j
          public class redistest {
              @resource
              private redisoperatingutil redisoperatingutil;
              @resource
              private submessagemapper submessagemapper;
          }
          

          接下来,我们看下submessage实体类的代码。

          package com.lk.entity;
          
          import lombok.allargsconstructor;
          import lombok.getter;
          import lombok.noargsconstructor;
          import lombok.setter;
          
          @getter
          @setter
          @noargsconstructor
          @allargsconstructor
          // 聊天记录-消息内容
          public class submessage {
            private integer id;
            private string msgtext; // 消息内容
            private string createtime; // 创建时间
            private string username; // 用户名
            private string userid; // 推送方用户id
            private string avatarsrc; // 推送方头像
            private string msgid; // 接收方用户id
            private boolean status; // 消息状态
          }
          
          

          测试list数据的写入与获取

          在单元测试类内部加入下述代码:

              @test
              public void testserializablelistredistemplate() {
                  // 构造聊天记录实体类数据
                  submessage submessage = new submessage();
                  submessage.setavatarsrc("https://www.kaisir.cn/uploads/1ece3749801d4d45933ba8b31403c685touxiang.jpeg");
                  submessage.setuserid("1090192");
                  submessage.setusername("神奇的程序员");
                  submessage.setmsgtext("你好");
                  submessage.setmsgid("2901872");
                  submessage.setcreatetime("2020-12-12 18:54:06");
                  submessage.setstatus(false);
                  // 将聊天记录对象保存到redis中
                  redisoperatingutil.listrightpush("submessage", submessage);
                  // 获取list中的数据
                  object resultobj = redisoperatingutil.listrange("submessage", 0, redisoperatingutil.listlen("submessage"));
                  // 将object安全的转为list
                  list<submessage> resultlist = objecttootherutil.castlist(resultobj, submessage.class);
                  // 遍历获取到的结果
                  if (resultlist != null) {
                      for (submessage message : resultlist) {
                          system.out.println(message.getusername());
                      }
                  }
              }
          

          在上述代码中,我们从redis中取出的数据是object类型的,我们要将它转换为与之对应的实体类,一开始我是用的类型强转,但是idea会报黄色警告,于是就写了一个工具类用于将object对象安全的转换为与之对应的类型,代码如下:

          package com.lk.utils;
          
          import java.util.arraylist;
          import java.util.list;
          
          public class objecttootherutil {
              public static <t> list<t> castlist(object obj, class<t> clazz) {
                  list<t> result = new arraylist<>();
                  if (obj instanceof list<?>) {
                      for (object o : (list<?>) obj) {
                          result.add(clazz.cast(o));
                      }
                      return result;
                  }
                  return null;
              }
          }
          
          

          执行后,我们看看redis是否有保存到我们写入的数据,如下所示,已经成功保存。

          我们再来看看,代码的执行结果,看看有没有成功获取到数据,如下图所示,也成功取到了。

          注意:如果你的项目对websocket进行了启动配置,可能会导致单元测试失败,报错java.lang.illegalstateexception: failed to load applicationcontext,解决方案就是注释掉websocket配置文件中的@configuration即可。

          测试list数据的取出

          当我们把redis中存储的数据迁移到mysql后,需要删除redis中的数据,一开始我用的是它的delete方法,但是他的delete方法只能删除与之匹配的值,不能选择一个区间进行删除,于是就决定用它的pop方法进行出栈操作。

          我们来测试下工具类中的listpopleftkey方法。

              @test
              public void testlistpop() {
                  long item = 0;
                  // 获取存储在redis中聊天记录的条数
                  long messagelistsize = redisoperatingutil.listlen("submessage");
                  for (int i = 0; i < messagelistsize; i++) {
                      // 从头向尾取出链表中的元素
                      submessage messageresult = (submessage) redisoperatingutil.listpopleftkey("submessage");
                      log.info(messageresult.getmsgtext());
                      item++;
                  }
                  log.info(item+"条数据已成功取出");
              }
          

          执行结果如下所示,成功取出了redis中存储的两条数据。

          测试聊天记录转移至数据库

          接下来我们在redis中放入三条数据用于测试

          我们测试下将redis中的数据取出,然后写入数据库,代码如下:

              // 测试聊天记录转移数据库
              @test
              public void testredistomysqltask() {
                  // 获取存储在redis中聊天记录的条数
                  long messagelistsize = redisoperatingutil.listlen("submessage");
                  // 写入数据库的数据总条数
                  long resultcount = 0;
                  for (int i = 0; i < messagelistsize; i++) {
                      // 从头到尾取出链表中的元素
                      submessage submessage= (submessage) redisoperatingutil.listpopleftkey("submessage");
                      // 向数据库写入数据
                      int result = submessagemapper.addmessagetextinfo(submessage);
                      if (result > 0) {
                          // 写入成功
                          resultcount++;
                      }
                  }
                  log.info(resultcount+ "条聊天记录,已写入数据库");
              }
          

          执行结果如下,数据已成功写入数据库且redis中的数据也被删除。

          解析客户端数据保存至redis

          完成上述操作后,我们redis那一块的东西就搞定了,接下来就可以实现将客户端的数据存到redis里了。

          这里有个坑,因为websocket服务类中用到了@component,会导致redis的工具类注入失败,出现null的情况,解决这个问题需要将当前类名声明为静态变量,然后在init中获取赋值redis工具类,代码如下:

              // 解决redis操作工具类注入为null的问题
              public static websocketserver websocketserver;
              @postconstruct
              public void init() {
                  websocketserver = this;
                  websocketserver.redisoperatingutil = this.redisoperatingutil;
              }
          

          在websocket服务的@onmessage注解中,收到客户端发送的消息,我们将其保存到redis中,代码如下:

              /**
               * 收到客户端消息后调用的方法
               *
               * @param message 客户端发送过来的消息
               *                // @param session 客户端会话
               */
              @onmessage
              public void onmessage(string message) {
                  // 客户端发送的消息
                  jsonobject jsreply = new jsonobject(message);
                  // 添加在线人数
                  jsreply.put("onlineusers", getonlinecount());
                  if (jsreply.has("buddyid")) {
                      // 获取推送方id
                      string userid = jsreply.getstring("userid");
                      // 获取被推送方id
                      string buddyid = jsreply.getstring("buddyid");
                      // 非测试数据则推送消息
                      if (!buddyid.equals("121710f399b84322bdecc238199d6888")) {
                          // 发送消息至推送方
                          this.sendinfo(jsreply.tostring(), userid);
                      }
                      // 构造聊天记录实体类数据
                      submessage submessage = new submessage();
                      submessage.setavatarsrc(jsreply.getstring("avatarsrc"));
                      submessage.setuserid(jsreply.getstring("userid"));
                      submessage.setusername(jsreply.getstring("username"));
                      submessage.setmsgtext(jsreply.getstring("msg"));
                      submessage.setmsgid(jsreply.getstring("msgid"));
                      submessage.setcreatetime(dateutil.getthistime());
                      submessage.setstatus(false);
                      // 将聊天记录对象保存到redis中
                      websocketserver.redisoperatingutil.listrightpush("submessage", submessage);
                      // 发送消息至被推送方
                      this.sendinfo(jsreply.tostring(), buddyid);
                  }
              }
          

          做完上述操作后,收到客户端发送的消息就会自动写入redis。

          定时将redis的数据写入mysql

          接下来,我们使用quartz定时向mysql中写入数据,他执行定时任务的步骤分为2步:

          1. 创建任务类编写任务内容
          2. 在quartzconfig文件中设置定时,执行第一步创建的任务。

          首先,创建quartzserver包,在其下创建redistomysqltask.java文件,在此文件内实现redis写入mysql的代码

          package com.lk.quartzserver;
          
          import com.lk.dao.submessagemapper;
          import com.lk.entity.submessage;
          import com.lk.utils.redisoperatingutil;
          import lombok.extern.slf4j.slf4j;
          import org.quartz.jobexecutioncontext;
          import org.quartz.jobexecutionexception;
          import org.springframework.scheduling.quartz.quartzjobbean;
          
          import javax.annotation.resource;
          
          // 将redis数据放进mysql中
          @slf4j
          public class redistomysqltask extends quartzjobbean {
              @resource
              private redisoperatingutil redisoperatingutil;
              @resource
              private submessagemapper submessagemapper;
          
              @override
              protected void executeinternal(jobexecutioncontext jobexecutioncontext) throws jobexecutionexception {
                  // 获取存储在redis中聊天记录的条数
                  long messagelistsize = redisoperatingutil.listlen("submessage");
                  // 写入数据库的数据总条数
                  long resultcount = 0;
                  for (int i = 0; i < messagelistsize; i++) {
                      // 从头到尾取出链表中的元素
                      submessage submessage= (submessage) redisoperatingutil.listpopleftkey("submessage");
                      // 向数据库写入数据
                      int result = submessagemapper.addmessagetextinfo(submessage);
                      if (result > 0) {
                          // 写入成功
                          resultcount++;
                      }
                  }
                  log.info(resultcount+ "条聊天记录,已写入数据库");
              }
          }
          
          

          在config包下创建quartzconfig.java文件,创建定时任务

          package com.lk.config;
          
          import com.lk.quartzserver.redistomysqltask;
          import org.quartz.*;
          import org.springframework.context.annotation.bean;
          import org.springframework.context.annotation.configuration;
          
          /**
           * quartz定时任务配置
           */
          @configuration
          public class quartzconfig {
              @bean
              public jobdetail redistomysqlquartz() {
                  // 执行定时任务
                  return jobbuilder.newjob(redistomysqltask.class).withidentity("callpayquartztask").storedurably().build();
              }
          
              @bean
              public trigger callpayquartztasktrigger() {
                  //cron方式,从每月1号开始,每隔三天就执行一次
                  return triggerbuilder.newtrigger().forjob(redistomysqlquartz())
                          .withidentity("callpayquartztask")
                          .withschedule(cronschedulebuilder.cronschedule("* * 4 1/3 * ?"))
                          .build();
              }
          }
          
          

          这里我设置的定时任务是从每月1号开始,每隔三天就执行一次,quartz定时任务采用的是cron表达式,自己算这个比较麻烦,这里推荐一个在线网站,可以很容易的生成表达式:cron表达式生成器

          实现效果

          最后,配合vue实现的浏览器端,跟大家展示下实现效果:

          效果视频:使用vue实现单聊

          项目浏览器端代码地址:github/chat-system

          项目在线体验地址:chat-system

          总结

          到此这篇关于利用redis实现聊天记录转存功能的文章就介绍到这了,更多相关redis聊天记录转存内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

          (0)
          上一篇 2022年3月21日
          下一篇 2022年3月21日

          相关推荐