關(guān)于Redis解決Session共享問(wèn)題
一、集群Session共享問(wèn)題
session共享問(wèn)題:多臺(tái)Tomcat并不共享session存儲(chǔ)空間,當(dāng)請(qǐng)求切換到不同tomcat服務(wù)器時(shí)導(dǎo)致數(shù)據(jù)丟失的問(wèn)題
tomcat可以進(jìn)行多臺(tái)tomcat進(jìn)行session拷貝,但是數(shù)據(jù)拷貝保存相同的內(nèi)容會(huì)存在資源浪費(fèi),而且會(huì)有時(shí)間延遲,所以這種方案不可行
session的替代方案應(yīng)該滿足:
- 數(shù)據(jù)共享
- 內(nèi)存存儲(chǔ)
- key、value結(jié)構(gòu)
這里我們可以使用redis
二、Redis存儲(chǔ)驗(yàn)證碼和對(duì)象
發(fā)送短信:
@Resource private StringRedisTemplate stringRedisTemplate; @Override public Result sendCode(String phone, HttpSession session) { // 1.校驗(yàn)手機(jī)號(hào) if (phone == null || str.matches("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$")) { // 2.如何不符合,返回錯(cuò)誤信息 return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!"); } // 3.符合,生成驗(yàn)證碼 String code = RandomUtil.randomNumbers(6); // 4.保存驗(yàn)證碼到Redis stringRedisTemplate.opsForValue().set("login:code:" + phone,code,2, TimeUnit.MINUTES); //具體的發(fā)送邏輯 在這里就不實(shí)現(xiàn)了 return Result.ok(); }
首先,我們會(huì)校驗(yàn)前端傳來(lái)的手機(jī)號(hào)格式,如果格式不正確直接返回。使用hutool的工具類(lèi)生成6位隨機(jī)驗(yàn)證碼,然后將驗(yàn)證碼作為value存入到Redis中,為了避免key重復(fù),我們?cè)O(shè)置了固定格式的key,并且設(shè)置一個(gè)2分鐘的超時(shí)時(shí)間,超過(guò)兩分鐘驗(yàn)證碼自動(dòng)失效。
登錄功能:
public Result login(LoginFormDTO loginForm, HttpSession session) { // 1. 校驗(yàn)手機(jī)號(hào) String phone = loginForm.getPhone(); if (phone == null || str.matches("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$")) { // 如何不符合,返回錯(cuò)誤信息 return Result.fail("手機(jī)號(hào)格式錯(cuò)誤!"); } // 2. 校驗(yàn)驗(yàn)證碼 String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone); String code = loginForm.getCode(); // 3. 不一致,報(bào)錯(cuò) if(cacheCode == null || !cacheCode.equals(code)) { return Result.fail("驗(yàn)證碼錯(cuò)誤!"); } // 4. 一致,根據(jù)手機(jī)號(hào)查詢用戶 select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); // 5. 判斷用戶是否存在 if (user == null) { // 6. 不存在,創(chuàng)建用戶并保存 user = createUserWithPhone(phone); } // 7. 保存用戶信息到Redis String token = UUID.randomUUID().toString(true); UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>() , CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString())); stringRedisTemplate.opsForHash().putAll("login:token:" + token,userMap); stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES); return Result.ok(token); }
我們?cè)谶M(jìn)行登錄時(shí),首先會(huì)對(duì)手機(jī)號(hào)格式進(jìn)行檢驗(yàn),如果手機(jī)號(hào)格式正確,我們從Redis中獲取驗(yàn)證碼和客戶端傳來(lái)的驗(yàn)證碼進(jìn)行比較,如果一致我們就放行,先去數(shù)據(jù)庫(kù)查詢?cè)撚脩粜畔?,如果用戶不存在進(jìn)行保存。
可能有的同學(xué)會(huì)有疑問(wèn),為什么這里要進(jìn)行這么麻煩的操作呢?
因?yàn)槲覀僓serDTO中的id是Long類(lèi)型的,會(huì)報(bào)Long轉(zhuǎn)String類(lèi)型轉(zhuǎn)換異常,因?yàn)槲覀冞@里使用的是StringRedisTemplate
該類(lèi)型要求key和value都是String類(lèi)型,但是我們將對(duì)象轉(zhuǎn)為Map時(shí),id為L(zhǎng)ong類(lèi)型,所以就出現(xiàn)了該問(wèn)題,兩種方案:1.自定義Map手動(dòng)put 2.使用BeanUtil,自定義規(guī)則
我們需要將用戶對(duì)象存儲(chǔ)在Redis中,這里用什么作為key呢?我們這里用token作為key,將token返回給客戶端,客戶端后面請(qǐng)求的時(shí)候使用該token來(lái)獲取value。
我們value保存對(duì)象時(shí),使用什么存儲(chǔ)呢?
1.String:
2.Hash:
我們這里使用Hash存儲(chǔ)對(duì)象,因?yàn)镠ash結(jié)構(gòu)可以將對(duì)象中的每個(gè)字段獨(dú)立存儲(chǔ),可以針對(duì)單個(gè)字段做CRUD,并且占用內(nèi)存更少。
我們使用UUID隨機(jī)生成token,但是我們value是哈希結(jié)構(gòu),我們使用BeanUtil將對(duì)象轉(zhuǎn)為Hash存儲(chǔ),因?yàn)镽edis是在內(nèi)存存儲(chǔ)的,如果一直只存會(huì)存在內(nèi)存不夠用的情況,所以我們這里仍然需要設(shè)置一個(gè)超時(shí)時(shí)間,那么設(shè)置多長(zhǎng)時(shí)間呢?我們這里模仿Session的只要超過(guò)30分鐘不訪問(wèn)就會(huì)銷(xiāo)毀。
但是我們現(xiàn)在設(shè)置的是,從設(shè)置開(kāi)始不管有沒(méi)有用戶訪問(wèn)30分鐘后都會(huì)銷(xiāo)毀,這樣肯定是不行的,我們需要和session一樣,只要有用戶訪問(wèn)我們就需要更新超時(shí)時(shí)間,那么怎么做呢?可以借助攔截器
我們的攔截器不是Spring創(chuàng)建的對(duì)象,所以我們無(wú)法使用注入的方式獲取StringRedisTemplate對(duì)象,我們需要使用構(gòu)造方法的方法,那么誰(shuí)來(lái)調(diào)用呢?
我們可以在MvcConfig注冊(cè)攔截器時(shí)傳入StringRedisTemplate對(duì)象由于我們多處都需要用到ThreadLocal存儲(chǔ)的對(duì)象,所以我們將ThreadLocal封裝成一個(gè)工具類(lèi):
public class UserHolder { private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>(); public static void saveUser(UserDTO user){ tl.set(user); } public static UserDTO getUser(){ return tl.get(); } public static void removeUser(){ tl.remove(); } }
public class LoginInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 獲取請(qǐng)求頭中的token String token = request.getHeader("authorization"); if(StrUtil.isBlank(token)) { response.setStatus(401); return false; } // 2. 使用token獲取Redis中的對(duì)象 Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token); // 3. 判斷用戶是否存在 if(userMap == null) { response.setStatus(401); return false; } // 4. 將Hash 格式轉(zhuǎn)為UserDTO對(duì)象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 5. 將用戶存入ThreadLocal中 UserHolder.saveUser(userDTO); // 6. 刷新token超時(shí)時(shí)間 stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
大家需要注意的是我們需要remove ThreadLocal,因?yàn)門(mén)hreadLocal可能會(huì)存在內(nèi)存泄露問(wèn)題,因?yàn)閺?qiáng)軟引用的問(wèn)題,這里我們不具體介紹。
三、解決狀態(tài)登錄刷新問(wèn)題
但是這樣會(huì)存在一些問(wèn)題,該攔截器只會(huì)攔截需要登錄的路徑,其他路徑是不會(huì)攔截了,也就不會(huì)進(jìn)行token有效期的刷新了。怎么解決呢? 新加一個(gè)全部路徑的攔截器
public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 獲取請(qǐng)求頭中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2. 基于token獲取Redis中的用戶 Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token); // 3. 判斷用戶是否存在 if(userMap == null) { return true; } // 將查詢到的Hash轉(zhuǎn)為UserDTO對(duì)象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 5. 存在 保存用戶到ThreadLocal UserHolder.saveUser(userDTO); stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
我們創(chuàng)建一個(gè)攔截全部路徑的攔截器來(lái)進(jìn)行token有效期的刷新
我們?cè)诘卿洈r截器里,只需要判斷ThreadLocal里是否存在有效的用戶,如果有放行,否則攔截。
public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ); registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**"); } }
我們?cè)谧?cè)刷新Token的攔截器,并且增加所有路徑。但是我們?nèi)绾伪WC刷新Token的攔截器在登錄攔截器之前執(zhí)行呢?其實(shí)在MvcConfig中注冊(cè)攔截器的順序也就是攔截的順序,但是這樣不保險(xiǎn)
其實(shí)我們?cè)赼ddInterceptor時(shí)會(huì)生成一個(gè)攔截器注冊(cè)器對(duì)象
攔截器注冊(cè)器中又有一個(gè)order屬性,默認(rèn)都是0,這個(gè)值決定攔截器的執(zhí)行順序,值越小執(zhí)行優(yōu)先級(jí)越高。
我們可以通過(guò)設(shè)置order來(lái)決定它們的執(zhí)行順序
到此這篇關(guān)于Redis解決Session共享問(wèn)題的文章就介紹到這了,更多相關(guān)Redis解決Session共享內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- PHP實(shí)現(xiàn)負(fù)載均衡session共享redis緩存操作示例
- SpringCloud實(shí)現(xiàn)Redis在各個(gè)微服務(wù)的Session共享問(wèn)題
- SpringSession+Redis實(shí)現(xiàn)集群會(huì)話共享的方法
- Spring整合redis(jedis)實(shí)現(xiàn)Session共享的過(guò)程
- nginx+redis實(shí)現(xiàn)session共享
- Laravel如何使用Redis共享Session
- spring boot整合redis實(shí)現(xiàn)shiro的分布式session共享的方法
相關(guān)文章
Redis簡(jiǎn)易延時(shí)隊(duì)列的實(shí)現(xiàn)示例
在實(shí)際的業(yè)務(wù)場(chǎng)景中,經(jīng)常會(huì)遇到需要延時(shí)處理的業(yè)務(wù),本文就來(lái)介紹有下Redis簡(jiǎn)易延時(shí)隊(duì)列的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Redis創(chuàng)建并修改Lua 環(huán)境的實(shí)現(xiàn)方法
為了在Redis服務(wù)器中執(zhí)行Lua腳本, Redis在服務(wù)器內(nèi)嵌了一個(gè)Lua環(huán)境, 并對(duì)這個(gè)Lua環(huán)境進(jìn)行了一系列修改,本文主要介紹了Redis創(chuàng)建并修改Lua 環(huán)境的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05redis實(shí)現(xiàn)簡(jiǎn)單分布式鎖
這篇文章主要介紹了redis實(shí)現(xiàn)簡(jiǎn)單分布式鎖,文中通過(guò)代碼示例講解的非常詳細(xì),需要的朋友可以參考下2013-09-09