提示:文章寫完后,目錄可以自動生成,如何生成可參考右邊的幫助文檔
文章目錄
- 前言
- 1. 退出登錄
- 1.1 后端
- 1.2 前端
- 2. 獲取當前用戶信息
- 3. C端用戶競賽列表功能
- 3.1 后端
- 3.2 Jmeter-基本操作
- 3.3 數據版本性能測試-壓力測試
- 3.4 redis版本-緩存結構設計
- 3.5 redis版本代碼開發
- 3.6 redis版本性能測試
- 總結
前言
1. 退出登錄
1.1 后端
后端直接拷貝代碼就可以了
但是我們點擊了退出登錄,用戶還是可以查看競賽和題目列表的,但是不能答題,怎么實現這種可以一些操作的功能呢–》網關—》配置白名單之類的就可以了
1.2 前端
先是home.vue
<template><div class="oj-main-layout"><div class="oj-main-layout-header"><div class="oj-main-layout-nav"><Navbar></Navbar></div></div><div ><img src="@/assets/images/log-banner.png" class="banner-img"></div></div><RouterView />
</template><script setup>
import Navbar from '@/components/Navbar.vue'
</script><style lang="scss" scoped>
.el-main {padding: 0;
}.oj-main-layout {// background-color: #f7f7f7;padding-top: 20px;.banner-img {max-width: 1520px;margin: 0 auto;border-radius: 16px;width: "100%"}.oj-main-layout-header {height: 60px;position: absolute;width: 100%;background: #fff;left: 0;top: 0;z-index: 3;overflow: hidden;}.oj-main-layout-nav {max-width: 1520px;min-width: 100%;margin: 0 auto;height: 60px;background: #fff;}// banner 圖.oj-ship-banner {max-width: 1520px;min-width: 1520;margin: 0 auto;width: 100%;height: 100%;height: 350px;// width: 1677px;color: #ffffff;background: url("@/assets/index_bg.png") left top no-repeat;background-size: cover;overflow: hidden;}
}
</style>
然后是我們自定義的組件Navbar
放在components
<template><div class="oj-navbar"><div class="oj-navbar-menus"><img class="oj-navbar-logo" src="@/assets/logo.png" /><el-menu router class="oj-navbar-menu" mode="horizontal"><el-menu-item index="/c-oj/home/question">題庫</el-menu-item><el-menu-item index="/c-oj/home/exam">競賽</el-menu-item></el-menu></div><div class="oj-navbar-users"><img v-if="isLogin" class="oj-message" @click="goMessage" src="@/assets/message/message.png" /><el-dropdown v-if="isLogin"><div class="oj-navbar-name"><img class="oj-head-image" v-if="isLogin" :src="userInfo.headImage" /><span>{{ userInfo.nickName }}</span></div><template #dropdown><el-dropdown-menu><el-dropdown-item @click="goUserDetail"><div class="oj-navabar-item"><span>個人中心</span></div></el-dropdown-item><el-dropdown-item @click="goMyExam"><div class="oj-navabar-item"><span>我的競賽</span></div></el-dropdown-item><el-dropdown-item><div class="oj-navabar-item"><span @click="handleLogout">退出登錄</span></div></el-dropdown-item></el-dropdown-menu></template></el-dropdown><span class="oj-navbar-login-btn" v-if="!isLogin" @click="goLogin">登錄</span></div></div>
</template><script setup>
import { reactive, ref } from 'vue';
import router from '@/router';
import { getToken, removeToken } from '@/utils/cookie';const isLogin = ref(false)
const userInfo = reactive({nickName: '',headImage: ''
})</script><style lang="scss" scoped>
.oj-navbar {display: flex;justify-content: space-between;align-items: center;padding: 0 20px;box-sizing: border-box;max-width: 1520px;margin: 0 auto;.oj-navbar-menus {display: flex;align-items: center;height: 50px;.el-menu-item {font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 20px;color: #222222;line-height: 28px;text-align: center;width: 42px;text-align: left;margin-right: 25px;}}.oj-navbar-logo {width: 38px;height: 38px;background: #32C5FF;border-radius: 8px;cursor: pointer;object-fit: contain;margin-right: 59px;}.oj-navbar-menu {// margin-left: 18px;width: 600px;border: none;.el-menu-item {font-size: 16px;font-weight: 500;background-color: transparent !important;transition: none;border: none;line-height: 60px;}}.oj-navbar-users {display: flex;align-items: center;}.oj-navbar-login-btn {line-height: 60px;display: inline-block;font-family: PingFangSC, PingFang SC;font-weight: 400;font-size: 18px;color: #222222;text-align: center;cursor: pointer;.line {display: inline-block;width: 25px;}}.oj-message {cursor: pointer;margin-top: 15px;}.oj-head-image {width: 30px;height: 30px;border-radius: 30px;margin-right: 10px;}.oj-navbar-name {cursor: pointer;margin-top: 15px;font-weight: 400;color: #000;margin-left: 15px;font-size: 20px;width: 100px;display: flex;align-items: center;}.oj-navabar-item {display: flex;align-items: center;justify-content: center;padding: 0 32px;}
}
</style>
然后是配置登錄按鈕
function goLogin(){router.push("/c-oj/login")
}
登錄成功以后還要修改登錄狀態了isLogin
我們先要判斷用戶狀態,第一token存不存在,存在了也不能說明登錄了,因為可能過期了,后端過期了,不會刪除token,所以還要請求后端,判斷token是否過期
<div class="oj-navbar-name"><img class="oj-head-image" v-if="isLogin" src="@/assets/images/headImage.jpg" /><span>CK</span></div>
我們先把這個寫死
這個也要請求后端,順便就可以判斷是否過期了
所以這個請求用戶頭像和昵稱的接口就可以判斷是否過期了
function checkToken(){if(getToken()){//判斷是否過期,獲取用戶信息isLogin.value = true;}
}checkToken()
這樣就成功了,獲取頭像和昵稱的接口還沒實現
最后是退出登錄的邏輯實現了
export function logoutService() {return service({url: "/user/logout",method: "delete",});
}
header已經在請求攔截器中添加了
async function handleLogout(){await ElMessageBox.confirm('退出登錄','溫馨提示',{confirmButtonText: '確認',cancelButtonText: '取消',type: 'warning',})await logoutService();removeToken();isLogin.value=false;
}
2. 獲取當前用戶信息
這個還可以判斷是否過期
在request.js里面,這個方法中的router.push(‘/c-oj/login’)不要了,沒有登錄就不要跳轉到登錄頁面了
如果已經過期了–》網關就過不去
@Data
public class LoginUserVO {private String nickName;private String headImage;
}
這個是返回的用戶數據,core中,那么這樣的話,就要在登錄的時候就把用戶昵稱存入redis
@Data
public class LoginUser {//存儲在redis中的用戶信息private Integer identity;private String nickName;private String headImage;
}
public String createToken(Long userId, String secret,Integer identity,String nickName,String headImage){Map<String, Object> claims = new HashMap<>();String userKey = UUID.fastUUID().toString();claims.put(JwtConstants.LOGIN_USER_ID, userId);claims.put(JwtConstants.LOGIN_USER_KEY, userKey);String token = JwtUtils.createToken(claims, secret);LoginUser loginUser = new LoginUser();loginUser.setIdentity(identity);//2表示管理員,1表示普通用戶loginUser.setNickName(nickName);loginUser.setHeadImage(headImage);redisService.setCacheObject(getTokenKey(userKey), loginUser, CacheConstants.EXPIRED, TimeUnit.MINUTES);return token;}
然后就是在登錄的時候存入圖片
這樣就成功了
管理員那里的createToken傳入null就可以了
然后就是前端了
export function infoService() {return service({url: "/user/info",method: "get",});
}
async function checkToken(){if(getToken()){//判斷是否過期,獲取用戶信息const ret = await infoService()Object.assign(userInfo,ret.data)isLogin.value = true;}
}
<div class="oj-navbar-name"><img class="oj-head-image" v-if="isLogin" :src="userInfo.headImage" /><span>{{ userInfo.nickName }}</span></div>
注意加了冒號的src是針對響應式數據的,沒有加冒號的就是真對字符串進行處理了
這樣以后就可以了
3. C端用戶競賽列表功能
3.1 后端
C端列表只展示已經發布的競賽
C端列表功能可以不登錄就使用了—》網關配置,m還有題目列表也不需要登錄,配一個統一前綴表示不用登錄就可以使用,比如semiLogin
未完賽指的是比賽還沒有開始和已經開始的,歷史競賽指的是比賽已結束了
用一個type字段來區分,這也是一個過濾條件,type為0表示沒有結束的競賽,type為1表示已經結束的競賽
開始寫代碼
先直接復制管理員的列表接口,然后在做一些修改
@Data
public class ExamQueryDTO extends PageQueryDTO {private String title;private String startTime;private String endTime;private Integer type;//0表示未結束的競賽,1表示已經結束的競賽
}
@Data
public class ExamVO {@JsonSerialize(using = ToStringSerializer.class)private Long examId;private String title;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime startTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime endTime;}
<select id="selectExamList" resultType="com.ck.friend.domain.exam.vo.ExamVO">SELECTte.exam_id,te.title,te.start_time,te.end_timeFROMtb_exam te<where>status = 1<if test="title !=null and title !='' ">AND te.title LIKE CONCAT('%',#{title},'%')</if><if test="startTime != null and startTime != '' ">AND te.start_time >= #{startTime}</if><if test="endTime != null and endTime != ''">AND te.end_time <= #{endTime}</if><if test="type == 0">AND te.end_time > Now()</if><if test="type == 1">AND te.end_time <= Now()</if></where>ORDER BYte.create_time DESC</select>
@GetMapping("/semiLogin/list")public TableDataInfo list(ExamQueryDTO examQueryDTO){log.info("獲取競賽列表信息,examQueryDTO:{}", examQueryDTO);return getTableDataInfo(examService.list(examQueryDTO));}
security:ignore:whites: - /**/login- /friend/user/sendCode- /friend/user/loginOrRegister- /**/semiLogin/**
這樣就成功了
記得還有攔截器也要配置一下
3.2 Jmeter-基本操作
Apache JMeter是Apache組織開發的基于Java的壓?測試?具。?于對軟件做壓?測試,它最初被設計?于Web應?測試,但后來擴展到其他測試領域。
下載官網
一個linux來服務部署
一個linux服務器來組件部署,比如redis,mysql,nacos
一個linux服務器來部署jemeter
壓縮之后,在bin目錄下找到jmeter.bat文件,雙擊就可以運行了。就可以啟動了
還有一個方法是打開cmd,輸入jmeter就可以打開的
----》注意這樣的話,就要添加環境變量的,就是添加bin目錄就可以了
但是我們的jmeter是英文的,可以變為中文的
在bin目錄下找到jmeter.properties
找到language配置,默認是en
改為language=zh_CN就可以了
點擊f12,中的network中的XHR可以找到發送的后端請求
先添加一個線程組
然后在添加http請求
點擊左上角的運行之后要保存才可以
但是我們直接運行看不到響應的數據,所以還要添加一個監聽器
這樣運行以后就可以看到結果那些了
這個就是10個請求,相當于10個用戶同時發送
點擊這個可以清楚結果樹
這樣就有10個了
3.3 數據版本性能測試-壓力測試
這樣寫的意思就是有1000個用戶在120s內不斷地發送請求
在linux執行jmeter
4.7/s
表示每s處理4.7個請求JPS
第一行表示42s內處理了201個請求,然后每s4.7個
=就是上面累加的和,是請求數和時間的累加
+表示是不同的數據
最終表示每s只能處理5.9個請求
所以性能不好
Err:表示錯誤數,和錯誤數占總數的比例
怎么提高性能呢----》redis
redis可以提高很多的性能
這樣也說明了mysql性能太低了
而C端肯定人很多的,所以我們一定要用redis提高速度
因為B端管理員太少了,所以不用管是否用redis提高速度
MySQL可以保證數據的準確性,redis保證速度
所以我們先從redis中查詢,如果沒有再去數據庫,在同步到redis
3.4 redis版本-緩存結構設計
發布競賽的時候可以存入redis,因為只有發布的競賽才會展示給用戶
取消發布的時候就從redis中移除
然后就是已經發布的競賽不允許修改(在前端的按鈕實現了),所以不用擔心redis數據不準確
然后就是選擇什么數據結構存儲到redis中
—》結構是有序的,可以按照開賽時間來排序,而且支持分頁
redis中有list這個數據結構–》支持分頁查詢
然后就未完賽和歷史競賽可以分開存儲到不同redis中,要用兩個list
key是什么—》exam:history:list和exam:time:list(未完賽)
然后就是還有一個list就是我報名的競賽列表,然后就是我報名的競賽列表一定是會和未完賽和歷史競賽重復一些的–》不好,重復次數太多了就不好了,所以就不要用list來存基本信息了,會重復的
—》一個競賽基本信息存一份,這樣就不會浪費了
value為String,json格式,key為exam:detail:examId
所以exam:history:list和exam:time:list(未完賽)存儲examId
exam:detail:examId存儲詳細信息,這樣就不會很浪費了
list存儲examId的,一份基本信息存一份String
3.5 redis版本代碼開發
注意發布的競賽肯定是一個還沒有開始,還沒結束的競賽
—》e:t:l,和e:d:exmaId
取消發布的競賽也是一個還沒有開始,還沒結束的競賽
查詢redis數據的時候,第一次(發布上線的時候自己調用)從數據庫中查詢,然后存入redis,后面才是從redis中查詢
現在system下面寫代碼
就是發布的時候加入redis,取消發布的時候從redis中刪除
創建一個manager的包
@Component
public class ExamCacheManager {@Autowiredprivate RedisService redisService;public void addCache(Exam exam) {redisService.leftPushForList(getExamListKey(), exam.getExamId());redisService.setCacheObject(getDetailKey(exam.getExamId()), exam);}public void deleteCache(Long examId) {redisService.removeForList(getExamListKey(), examId);redisService.deleteObject(getDetailKey(examId));}private String getExamListKey() {return CacheConstants.EXAM_UNFINISHED_LIST;}private String getDetailKey(Long examId) {return CacheConstants.EXAM_DETAIL + examId;}}
leftPushForList往左邊插入,這個就相當于是把早創建的放到前面了
public final static String EXAM_UNFINISHED_LIST = "exam:time:list"; // 未完賽競賽列表public final static String EXAM_HISTORY_LIST = "exam:history:list"; // 歷史競賽列表public final static String EXAM_DETAIL = "exam:detail:"; //競賽詳情信息
然后就是修改cancelPublish和publish的代碼
把這兩個方法添加進去就可以了
這兩個就是對redis的增加和刪除了
然后就是對redis的查詢了
至于對redis的修改呢
我們規定
已經發布的,或者已經開賽的,已經存入redis的,不能修改exam相關數據
撤銷發布的,沒有存入redis的才可以修改exam,這樣就沒有問題了
然后就是redis的查詢了
在friend中
@AllArgsConstructor
@Getter
public enum ExamListType {EXAM_UN_FINISH_LIST(0),EXAM_HISTORY_LIST(1);private final Integer value;}
public <T> List<T> multiGet(final List<String> keyList, Class<T> clazz) {List list = redisTemplate.opsForValue().multiGet(keyList);if (list == null || list.size() <= 0) {return null;}List<T> result = new ArrayList<>();for (Object o : list) {result.add(JSON.parseObject(String.valueOf(o), clazz));}return result;}public <K, V> void multiSet(Map<? extends K, ? extends V> map) {redisTemplate.opsForValue().multiSet(map);}
在RedisService中插入這兩個方法。這兩個方法是批量處理的方法,就是對examId的list批量從redis中獲取詳細數據,或者批量設置數據,不然一個一個設置,效率還是太低了
還是創建一個manager的包
@Component
public class ExamCacheManager {@Autowiredprivate ExamMapper examMapper;@Autowiredprivate RedisService redisService;public Long getListSize(Integer examListType) {String examListKey = getExamListKey(examListType);return redisService.getListSize(examListKey);}public List<ExamVO> getExamVOList(ExamQueryDTO examQueryDTO) {int start = (examQueryDTO.getPageNum() - 1) * examQueryDTO.getPageSize();int end = start + examQueryDTO.getPageSize() - 1; //下標需要 -1String examListKey = getExamListKey(examQueryDTO.getType());List<Long> examIdList = redisService.getCacheListByRange(examListKey, start, end, Long.class);List<ExamVO> examVOList = assembleExamVOList(examIdList);if (CollectionUtil.isEmpty(examVOList)) {//說明redis中數據可能有問題 從數據庫中查數據并且重新刷新緩存examVOList = getExamListByDB(examQueryDTO); //從數據庫中獲取數據refreshCache(examQueryDTO.getType());}return examVOList;}//刷新緩存邏輯public void refreshCache(Integer examListType) {List<Exam> examList = new ArrayList<>();if (ExamListType.EXAM_UN_FINISH_LIST.getValue().equals(examListType)) {//查詢未完賽的競賽列表examList = examMapper.selectList(new LambdaQueryWrapper<Exam>().select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime).gt(Exam::getEndTime, LocalDateTime.now()).eq(Exam::getStatus, Constants.TRUE).orderByDesc(Exam::getCreateTime));} else if (ExamListType.EXAM_HISTORY_LIST.getValue().equals(examListType)) {//查詢歷史競賽examList = examMapper.selectList(new LambdaQueryWrapper<Exam>().select(Exam::getExamId, Exam::getTitle, Exam::getStartTime, Exam::getEndTime).le(Exam::getEndTime, LocalDateTime.now()).eq(Exam::getStatus, Constants.TRUE).orderByDesc(Exam::getCreateTime));}if (CollectionUtil.isEmpty(examList)) {return;}Map<String, Exam> examMap = new HashMap<>();List<Long> examIdList = new ArrayList<>();for (Exam exam : examList) {examMap.put(getDetailKey(exam.getExamId()), exam);examIdList.add(exam.getExamId());}redisService.multiSet(examMap); //刷新詳情緩存redisService.deleteObject(getExamListKey(examListType));redisService.rightPushAll(getExamListKey(examListType), examIdList); //刷新列表緩存}private List<ExamVO> getExamListByDB(ExamQueryDTO examQueryDTO) {PageHelper.startPage(examQueryDTO.getPageNum(), examQueryDTO.getPageSize());return examMapper.selectExamList(examQueryDTO);}private List<ExamVO> assembleExamVOList(List<Long> examIdList) {if (CollectionUtil.isEmpty(examIdList)) {//說明redis當中沒數據 從數據庫中查數據并且重新刷新緩存return null;}//拼接redis當中key的方法 并且將拼接好的key存儲到一個list中List<String> detailKeyList = new ArrayList<>();for (Long examId : examIdList) {detailKeyList.add(getDetailKey(examId));}List<ExamVO> examVOList = redisService.multiGet(detailKeyList, ExamVO.class);CollUtil.removeNull(examVOList);if (CollectionUtil.isEmpty(examVOList) || examVOList.size() != examIdList.size()) {//說明redis中數據有問題 從數據庫中查數據并且重新刷新緩存return null;}return examVOList;}private String getExamListKey(Integer examListType) {if (ExamListType.EXAM_UN_FINISH_LIST.getValue().equals(examListType)) {return CacheConstants.EXAM_UNFINISHED_LIST;} else if (ExamListType.EXAM_HISTORY_LIST.getValue().equals(examListType)) {return CacheConstants.EXAM_HISTORY_LIST;}return null;}private String getDetailKey(Long examId) {return CacheConstants.EXAM_DETAIL + examId;}}
現在開始挨個講一下這些方法的使用
getListSize是根據key查出對應的list的元素數量
getExamVOList是根據service傳入的參數進行分頁查詢
其實就是根據頁數和每頁數量進行下標的查詢罷了
中的assembleExamVOList是根據從redis查出的examId再從redis查出對應的exam詳細數據
getExamListByDB中的CollUtil.removeNull(examVOList);就是移除數組中null的元素
refreshCache是刷新緩存,從數據庫中查詢exam列表數據,和詳細數據
將詳細數據直接multiSet,將列表數據,先deleteObject,在rightPushAll
是尾插
然后就可以寫正式的代碼了
@GetMapping("/semiLogin/redis/list")public TableDataInfo redisList(ExamQueryDTO examQueryDTO){log.info("獲取競賽列表信息,examQueryDTO:{}", examQueryDTO);return examService.redisList(examQueryDTO);}
@Overridepublic TableDataInfo redisList(ExamQueryDTO examQueryDTO) {Long listSize = examCacheManager.getListSize(examQueryDTO.getType());List<ExamVO> list;TableDataInfo tableDataInfo =new TableDataInfo();if(listSize==null||listSize==0){//說明緩存中沒有數據,所以要先從數據庫中獲取數據,然后存入redislist = list(examQueryDTO);examCacheManager.refreshCache(examQueryDTO.getType());long total = new PageInfo<>(list).getTotal();return TableDataInfo.success(list, total);}else{//直接從redis中獲取數據list = examCacheManager.getExamVOList(examQueryDTO);listSize = examCacheManager.getListSize(examQueryDTO.getType());return TableDataInfo.success(list, listSize);}}
這樣就可以了
這樣就可以了
然后就是競賽慢慢就變為結束了的,怎么轉移redis數據呢–》后面再說
3.6 redis版本性能測試
再次進行壓力測試
我們在Windows下用jmeter進行測試,發現快得多有了redis之后,相比以前
然后在linux下的jmeter進行測試
發現速度直接提升了幾十倍