您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)redis中怎么實(shí)現(xiàn)聊天記錄轉(zhuǎn)存功能,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
我的項(xiàng)目是基于SpringBoot2.x搭建的,電腦已經(jīng)安裝了redis,用的maven作為jar包管理工具,所以只需要在maven中添加需要的依賴包即可,如果你用的是其他管理工具,請(qǐng)自行查閱如何添加依賴。
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 定時(shí)任務(wù)調(diào)度 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>2.3.7.RELEASE</version> </dependency>
本文需要用到依賴:Redis 、quartz,在pom.xml文件的dependencies標(biāo)簽下添加下述代碼。
spring: # redis配置 redis: host: 127.0.0.1 # redis地址 port: 6379 # 端口號(hào) password: # 密碼 timeout: 3000 # 連接超時(shí)時(shí)間,單位毫秒
在websocket的服務(wù)中,收到客戶端推送的消息后,我們對(duì)數(shù)據(jù)進(jìn)行解析,構(gòu)造聊天記錄實(shí)體類(lèi),將其保存至redis中,最后我們使用quartz設(shè)置定時(shí)任務(wù)將redis的數(shù)據(jù)定時(shí)寫(xiě)入mysql中。
我們將上述思路進(jìn)行下整理:
解析客戶端數(shù)據(jù),構(gòu)造實(shí)體類(lèi)
將數(shù)據(jù)保存至redis
使用quartz將redis中的數(shù)據(jù)定時(shí)寫(xiě)入mysql
實(shí)現(xiàn)思路很簡(jiǎn)單,難在如何將實(shí)體類(lèi)數(shù)據(jù)保存至redis,我們需要把redis這一塊配置好后,才能繼續(xù)實(shí)現(xiàn)我們的業(yè)務(wù)需求。
redis支持的數(shù)據(jù)結(jié)構(gòu)類(lèi)型有:
set 集合,string類(lèi)型的無(wú)序集合,元素不允許重復(fù)
hash 哈希表,鍵值對(duì)的集合,用于存儲(chǔ)對(duì)象
list 列表,鏈表結(jié)構(gòu)
zset有序集合
string 字符串,最基本的數(shù)據(jù)類(lèi)型,可以包含任何數(shù)據(jù),比如一個(gè)序列化的對(duì)象,它的字符串大小上限是512MB
redis的客戶端分為jedis 和 lettuce,在SpringBoot2.x中默認(rèn)客戶端是使用lettuce實(shí)現(xiàn)的,因此我們不用做過(guò)多配置,在使用的時(shí)候通過(guò)RedisTemplate.xxx來(lái)對(duì)redis進(jìn)行操作即可。
在RedisTemplate中,默認(rèn)是使用Java字符串序列化,將字符串存入redis后可讀性很差,因此,我們需要對(duì)他進(jìn)行自定義,使用Jackson 序列化,以 JSON 方式進(jìn)行存儲(chǔ)。
我們?cè)陧?xiàng)目的config包下,創(chuàng)建一個(gè)名為L(zhǎng)ettuceRedisConfig的Java文件,我們?cè)俅宋募信渲闷淠J(rèn)序列化規(guī)則,它的代碼如下:
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設(shè)置序列化器, 方便轉(zhuǎn)換redis中的數(shù)據(jù)與實(shí)體類(lèi)互轉(zhuǎn) @Configuration public class LettuceRedisConfig { /** * Redis 序列化配置 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); // 使用GenericJackson2JsonRedisSerializer替換默認(rèn)序列化 GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); // 設(shè)置 Key 和 Value 的序列化規(guī)則 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // 初始化 RedisTemplate 序列化完成 redisTemplate.afterPropertiesSet(); return redisTemplate; } }
做完上述操作后,通過(guò)RedisTemplate存儲(chǔ)到redis中的數(shù)據(jù)就是json形式的了,接下來(lái)我們對(duì)其常用的操作封裝成工具類(lèi),方便我們?cè)陧?xiàng)目中使用。
在Utils包中創(chuàng)建一個(gè)名為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操作工具類(lèi) public class RedisOperatingUtil { @Resource private RedisTemplate<Object, Object> redisTemplate; /** * 指定 key 的過(guò)期時(shí)間 * * @param key 鍵 * @param time 時(shí)間(秒) */ public void setKeyTime(String key, long time) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } /** * 根據(jù) key 獲取過(guò)期時(shí)間(-1 即為永不過(guò)期) * * @param key 鍵 * @return 過(guò)期時(shí)間 */ 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 的類(lèi)型 * * @param key 鍵 */ public String keyType(String key) { DataType dataType = redisTemplate.type(key); assert dataType != null; return dataType.code(); } /** * 批量設(shè)置值 * * @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)); } /** * 獲取指定對(duì)象類(lèi)型key的值 * * @param key 鍵 * @return 值 */ public Object objectGetKey(String key) { return redisTemplate.opsForValue().get(key); } /** * 設(shè)置對(duì)象類(lèi)型的數(shù)據(jù) * * @param key 鍵 * @param value 值 */ public void objectSetValue(String key, Object value) { redisTemplate.opsForValue().set(key, value); } /** * 向list的頭部插入一條數(shù)據(jù) * * @param key 鍵 * @param value 值 */ public Long listLeftPush(String key, Object value) { return redisTemplate.opsForList().leftPush(key, value); } /** * 向list的末尾插入一條數(shù)據(jù) * * @param key 鍵 * @param value 值 */ public Long listRightPush(String key, Object value) { return redisTemplate.opsForList().rightPush(key, value); } /** * 向list頭部添加list數(shù)據(jù) * * @param key 鍵 * @param value 值 */ public Long listLeftPushAll(String key, List<Object> value) { return redisTemplate.opsForList().leftPushAll(key, value); } /** * 向list末尾添加list數(shù)據(jù) * * @param key 鍵 * @param value 值 */ public Long listRightPushAll(String key, List<Object> value) { return redisTemplate.opsForList().rightPushAll(key, value); } /** * 通過(guò)索引設(shè)置list元素的值 * * @param key 鍵 * @param index 索引 * @param value 值 */ public void listIndexSet(String key, long index, Object value) { redisTemplate.opsForList().set(key, index, value); } /** * 獲取列表指定范圍內(nèi)的list元素,正數(shù)則表示正向查找,負(fù)數(shù)則倒敘查找 * * @param key 鍵 * @param start 開(kāi)始 * @param end 結(jié)束 * @return boolean */ public Object listRange(String key, long start, long end) { return redisTemplate.opsForList().range(key, start, end); } /** * 從列表前端開(kāi)始取出數(shù)據(jù) * * @param key 鍵 * @return 結(jié)果數(shù)組對(duì)象 */ public Object listPopLeftKey(String key) { return redisTemplate.opsForList().leftPop(key); } /** * 從列表末尾開(kāi)始遍歷取出數(shù)據(jù) * * @param key 鍵 * @return 結(jié)果數(shù)組 */ public Object listPopRightKey(String key) { return redisTemplate.opsForList().rightPop(key); } /** * 獲取list長(zhǎng)度 * * @param key 鍵 * @return 列表長(zhǎng)度 */ public Long listLen(String key) { return redisTemplate.opsForList().size(key); } /** * 通過(guò)索引獲取list中的元素 * * @param key 鍵 * @param index 索引(index>=0時(shí),0 表頭,1 第二個(gè)元素,依次類(lèi)推;index<0時(shí),-1,表尾,-2倒數(shù)第二個(gè)元素,依次類(lèi)推) * @return 列表中的元素 */ public Object listIndex(String key, long index) { return redisTemplate.opsForList().index(key, index); } /** * 移除list元素 * * @param key 鍵 * @param count 移除數(shù)量("負(fù)數(shù)"則從列表倒敘查找刪除 count 個(gè)對(duì)應(yīng)的值; "整數(shù)"則從列表正序查找刪除 count 個(gè)對(duì)應(yīng)的值;) * @param value 值 * @return 成功移除的個(gè)數(shù) */ public Long listRem(String key, long count, Object value) { return redisTemplate.opsForList().remove(key, count, value); } /** * 截取指定范圍內(nèi)的數(shù)據(jù), 移除不是范圍內(nèi)的數(shù)據(jù) * @param key 操作的key * @param start 截取開(kāi)始位置 * @param end 截取激素位置 */ public void listTrim(String key, long start, long end) { redisTemplate.opsForList().trim(key, start, end); } }
做完上述操作后,最難弄的一關(guān)我們就已經(jīng)搞定了,接下來(lái)我們來(lái)對(duì)一會(huì)需要使用的方法進(jìn)行單元測(cè)試,確保其能夠正常運(yùn)行。
創(chuàng)建一個(gè)名為RedisTest的Java文件,注入需要用到的相關(guān)類(lèi)。
redisOperatingUtil為我們的redis工具類(lèi)
subMessageMapper為聊天記錄表的dao層
@RunWith(SpringRunner.class) @SpringBootTest @Slf4j public class RedisTest { @Resource private RedisOperatingUtil redisOperatingUtil; @Resource private SubMessageMapper subMessageMapper; }
接下來(lái),我們看下SubMessage實(shí)體類(lèi)的代碼。
package com.lk.entity; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor // 聊天記錄-消息內(nèi)容 public class SubMessage { private Integer id; private String msgText; // 消息內(nèi)容 private String createTime; // 創(chuàng)建時(shí)間 private String userName; // 用戶名 private String userId; // 推送方用戶id private String avatarSrc; // 推送方頭像 private String msgId; // 接收方用戶id private Boolean status; // 消息狀態(tài) }
在單元測(cè)試類(lèi)內(nèi)部加入下述代碼:
@Test public void testSerializableListRedisTemplate() { // 構(gòu)造聊天記錄實(shí)體類(lèi)數(shù)據(jù) 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); // 將聊天記錄對(duì)象保存到redis中 redisOperatingUtil.listRightPush("subMessage", subMessage); // 獲取list中的數(shù)據(jù) Object resultObj = redisOperatingUtil.listRange("subMessage", 0, redisOperatingUtil.listLen("subMessage")); // 將Object安全的轉(zhuǎn)為L(zhǎng)ist List<SubMessage> resultList = ObjectToOtherUtil.castList(resultObj, SubMessage.class); // 遍歷獲取到的結(jié)果 if (resultList != null) { for (SubMessage message : resultList) { System.out.println(message.getUserName()); } } }
在上述代碼中,我們從redis中取出的數(shù)據(jù)是Object類(lèi)型的,我們要將它轉(zhuǎn)換為與之對(duì)應(yīng)的實(shí)體類(lèi),一開(kāi)始我是用的類(lèi)型強(qiáng)轉(zhuǎn),但是idea會(huì)報(bào)黃色警告,于是就寫(xiě)了一個(gè)工具類(lèi)用于將Object對(duì)象安全的轉(zhuǎn)換為與之對(duì)應(yīng)的類(lèi)型,代碼如下:
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; } }
執(zhí)行后,我們看看redis是否有保存到我們寫(xiě)入的數(shù)據(jù),如下所示,已經(jīng)成功保存。
我們?cè)賮?lái)看看,代碼的執(zhí)行結(jié)果,看看有沒(méi)有成功獲取到數(shù)據(jù),如下圖所示,也成功取到了。
注意:如果你的項(xiàng)目對(duì)websocket進(jìn)行了啟動(dòng)配置,可能會(huì)導(dǎo)致單元測(cè)試失敗,報(bào)錯(cuò)java.lang.IllegalStateException: Failed to load ApplicationContext,解決方案就是注釋掉websocket配置文件中的@Configuration即可。
當(dāng)我們把redis中存儲(chǔ)的數(shù)據(jù)遷移到mysql后,需要?jiǎng)h除redis中的數(shù)據(jù),一開(kāi)始我用的是它的delete方法,但是他的delete方法只能刪除與之匹配的值,不能選擇一個(gè)區(qū)間進(jìn)行刪除,于是就決定用它的pop方法進(jìn)行出棧操作。
我們來(lái)測(cè)試下工具類(lèi)中的listPopLeftKey方法。
@Test public void testListPop() { long item = 0; // 獲取存儲(chǔ)在redis中聊天記錄的條數(shù) 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+"條數(shù)據(jù)已成功取出"); }
執(zhí)行結(jié)果如下所示,成功取出了redis中存儲(chǔ)的兩條數(shù)據(jù)。
接下來(lái)我們?cè)趓edis中放入三條數(shù)據(jù)用于測(cè)試
我們測(cè)試下將redis中的數(shù)據(jù)取出,然后寫(xiě)入數(shù)據(jù)庫(kù),代碼如下:
// 測(cè)試聊天記錄轉(zhuǎn)移數(shù)據(jù)庫(kù) @Test public void testRedisToMysqlTask() { // 獲取存儲(chǔ)在redis中聊天記錄的條數(shù) long messageListSize = redisOperatingUtil.listLen("subMessage"); // 寫(xiě)入數(shù)據(jù)庫(kù)的數(shù)據(jù)總條數(shù) long resultCount = 0; for (int i = 0; i < messageListSize; i++) { // 從頭到尾取出鏈表中的元素 SubMessage subMessage= (SubMessage) redisOperatingUtil.listPopLeftKey("subMessage"); // 向數(shù)據(jù)庫(kù)寫(xiě)入數(shù)據(jù) int result = subMessageMapper.addMessageTextInfo(subMessage); if (result > 0) { // 寫(xiě)入成功 resultCount++; } } log.info(resultCount+ "條聊天記錄,已寫(xiě)入數(shù)據(jù)庫(kù)"); }
執(zhí)行結(jié)果如下,數(shù)據(jù)已成功寫(xiě)入數(shù)據(jù)庫(kù)且redis中的數(shù)據(jù)也被刪除。
完成上述操作后,我們r(jià)edis那一塊的東西就搞定了,接下來(lái)就可以實(shí)現(xiàn)將客戶端的數(shù)據(jù)存到redis里了。
這里有個(gè)坑,因?yàn)閣ebsocket服務(wù)類(lèi)中用到了@Component,會(huì)導(dǎo)致redis的工具類(lèi)注入失敗,出現(xiàn)null的情況,解決這個(gè)問(wèn)題需要將當(dāng)前類(lèi)名聲明為靜態(tài)變量,然后在init中獲取賦值redis工具類(lèi),代碼如下:
// 解決redis操作工具類(lèi)注入為null的問(wèn)題 public static WebSocketServer webSocketServer; @PostConstruct public void init() { webSocketServer = this; webSocketServer.redisOperatingUtil = this.redisOperatingUtil; }
在websocket服務(wù)的@OnMessage注解中,收到客戶端發(fā)送的消息,我們將其保存到redis中,代碼如下:
/** * 收到客戶端消息后調(diào)用的方法 * * @param message 客戶端發(fā)送過(guò)來(lái)的消息 * // @param session 客戶端會(huì)話 */ @OnMessage public void onMessage(String message) { // 客戶端發(fā)送的消息 JSONObject jsReply = new JSONObject(message); // 添加在線人數(shù) jsReply.put("onlineUsers", getOnlineCount()); if (jsReply.has("buddyId")) { // 獲取推送方id String userId = jsReply.getString("userID"); // 獲取被推送方id String buddyId = jsReply.getString("buddyId"); // 非測(cè)試數(shù)據(jù)則推送消息 if (!buddyId.equals("121710f399b84322bdecc238199d6888")) { // 發(fā)送消息至推送方 this.sendInfo(jsReply.toString(), userId); } // 構(gòu)造聊天記錄實(shí)體類(lèi)數(shù)據(jù) 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); // 將聊天記錄對(duì)象保存到redis中 webSocketServer.redisOperatingUtil.listRightPush("subMessage", subMessage); // 發(fā)送消息至被推送方 this.sendInfo(jsReply.toString(), buddyId); } }
做完上述操作后,收到客戶端發(fā)送的消息就會(huì)自動(dòng)寫(xiě)入redis。
接下來(lái),我們使用quartz定時(shí)向mysql中寫(xiě)入數(shù)據(jù),他執(zhí)行定時(shí)任務(wù)的步驟分為2步:
創(chuàng)建任務(wù)類(lèi)編寫(xiě)任務(wù)內(nèi)容
在QuartzConfig文件中設(shè)置定時(shí),執(zhí)行第一步創(chuàng)建的任務(wù)。
首先,創(chuàng)建quartzServer包,在其下創(chuàng)建RedisToMysqlTask.java文件,在此文件內(nèi)實(shí)現(xiàn)redis寫(xiě)入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數(shù)據(jù)放進(jìn)mysql中 @Slf4j public class RedisToMysqlTask extends QuartzJobBean { @Resource private RedisOperatingUtil redisOperatingUtil; @Resource private SubMessageMapper subMessageMapper; @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { // 獲取存儲(chǔ)在redis中聊天記錄的條數(shù) long messageListSize = redisOperatingUtil.listLen("subMessage"); // 寫(xiě)入數(shù)據(jù)庫(kù)的數(shù)據(jù)總條數(shù) long resultCount = 0; for (int i = 0; i < messageListSize; i++) { // 從頭到尾取出鏈表中的元素 SubMessage subMessage= (SubMessage) redisOperatingUtil.listPopLeftKey("subMessage"); // 向數(shù)據(jù)庫(kù)寫(xiě)入數(shù)據(jù) int result = subMessageMapper.addMessageTextInfo(subMessage); if (result > 0) { // 寫(xiě)入成功 resultCount++; } } log.info(resultCount+ "條聊天記錄,已寫(xiě)入數(shù)據(jù)庫(kù)"); } }
在config包下創(chuàng)建QuartzConfig.java文件,創(chuàng)建定時(shí)任務(wù)
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定時(shí)任務(wù)配置 */ @Configuration public class QuartzConfig { @Bean public JobDetail RedisToMysqlQuartz() { // 執(zhí)行定時(shí)任務(wù) return JobBuilder.newJob(RedisToMysqlTask.class).withIdentity("CallPayQuartzTask").storeDurably().build(); } @Bean public Trigger CallPayQuartzTaskTrigger() { //cron方式,從每月1號(hào)開(kāi)始,每隔三天就執(zhí)行一次 return TriggerBuilder.newTrigger().forJob(RedisToMysqlQuartz()) .withIdentity("CallPayQuartzTask") .withSchedule(CronScheduleBuilder.cronSchedule("* * 4 1/3 * ?")) .build(); } }
關(guān)于redis中怎么實(shí)現(xiàn)聊天記錄轉(zhuǎn)存功能就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。