在上一節《淘東電商項目(20) -會員惟一登陸》,主要講解會員如何實現三端惟一登陸。java
本文代碼已提交至Github(版本號:
31112e64e8bc832a1416c2fcfd064b5e45b45f32
),有興趣的同窗能夠下載來看看:https://github.com/ylw-github/taodong-shopgit
本文講解會員服務中數據庫狀態與Redis服務狀態如何保持一致性。github
本文目錄結構:
l____引言
l____ 1. 問題引出
l____ 2. 解決思路
l____ 3. 代碼實現
l____ 4. 測試
l____ 5. 第三方框架推薦
l____總結redis
下面先來貼一下登陸接口的代碼:數據庫
@Override public BaseResponse<JSONObject> login(@RequestBody UserLoginInDTO userLoginInpDTO) { // 1.驗證參數 String mobile = userLoginInpDTO.getMobile(); if (StringUtils.isEmpty(mobile)) { return setResultError("手機號碼不能爲空!"); } String password = userLoginInpDTO.getPassword(); if (StringUtils.isEmpty(password)) { return setResultError("密碼不能爲空!"); } // 判斷登錄類型 String loginType = userLoginInpDTO.getLoginType(); if (StringUtils.isEmpty(loginType)) { return setResultError("登錄類型不能爲空!"); } // 目的是限制範圍 if (!(loginType.equals(Constants.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_IOS) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_PC))) { return setResultError("登錄類型出現錯誤!"); } // 設備信息 String deviceInfor = userLoginInpDTO.getDeviceInfor(); if (StringUtils.isEmpty(deviceInfor)) { return setResultError("設備信息不能爲空!"); } // 2.對登錄密碼實現加密 String newPassWord = MD5Util.MD5(password); // 3.使用手機號碼+密碼查詢數據庫 ,判斷用戶是否存在 UserDo userDo = userMapper.login(mobile, newPassWord); if (userDo == null) { return setResultError("用戶名稱或者密碼錯誤!"); } // 用戶登錄Token Session 區別 // 用戶每個端登錄成功以後,會對應生成一個token令牌(臨時且惟一)存放在redis中做爲rediskey value userid // 4.獲取userid Long userId = userDo.getUserId(); // 5.根據userId+loginType 查詢當前登錄類型帳號以前是否有登錄過,若是登錄過 清除以前redistoken UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType); if (userTokenDo != null) { // 若是登錄過 清除以前redistoken String token = userTokenDo.getToken(); Boolean isremoveToken = generateToken.removeToken(token); if (isremoveToken) { // 把該token的狀態改成1 userTokenMapper.updateTokenAvailability(token); } } // .生成對應用戶令牌存放在redis中 String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType; String newToken = generateToken.createToken(keyPrefix, userId + ""); // 1.插入新的token UserTokenDo userToken = new UserTokenDo(); userToken.setUserId(userId); userToken.setLoginType(userLoginInpDTO.getLoginType()); userToken.setToken(newToken); userToken.setDeviceInfor(deviceInfor); userTokenMapper.insertUserToken(userToken); JSONObject data = new JSONObject(); data.put("token", newToken); return setResultSuccess(data); }
咱們能夠看到代碼流程圖是這樣的:
能夠注意到流程圖裏,Redis和數據庫的操做是同步的,那若是插入Token到Redis成功了,可是插入Token到數據庫的時候失敗了,如何解決呢?markdown
這就是本文主要講的內容了,Redis如何與數據庫狀態保持一致?app
能夠看到上面出現的問題,很容易讓咱們聯想起「「事務」」,事務能夠保持ACID
,咱們知道數據庫是有事務的,Redis也有事務?那可否把這二者同時使用呢?好比以下場景:框架
- 若是redis更新操做失敗時,數據庫更新操做也要失敗
- 若是數據庫更新操做失敗時,Redis更新操做也要失敗
其實解決方案已經顯露出來了,咱們能夠重寫數據庫的事務和Redis事務,把二者合成一種新的事務解決方案,知足:ide
begin
)commit
)rollback
)1.先貼上數據庫事務與Redis事務的合成工具類:工具
/** * description: Redis與 DataSource 事務封裝 * create by: YangLinWei * create time: 2020/3/4 3:34 下午 */ @Component @Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE) public class RedisDataSoureceTransaction { @Autowired private RedisUtil redisUtil; /** * 數據源事務管理器 */ @Autowired private DataSourceTransactionManager dataSourceTransactionManager; /** * 開始事務 採用默認傳播行爲 * * @return */ public TransactionStatus begin() { // 手動begin數據庫事務 TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute()); redisUtil.begin(); return transaction; } /** * 提交事務 * * @param transactionStatus * 事務傳播行爲 * @throws Exception */ public void commit(TransactionStatus transactionStatus) throws Exception { if (transactionStatus == null) { throw new Exception("transactionStatus is null"); } // 支持Redis與數據庫事務同時提交 dataSourceTransactionManager.commit(transactionStatus); //redisUtil.exec();//會出錯,自動提交 } /** * 回滾事務 * * @param transactionStatus * @throws Exception */ public void rollback(TransactionStatus transactionStatus) throws Exception { if (transactionStatus == null) { throw new Exception("transactionStatus is null"); } dataSourceTransactionManager.rollback(transactionStatus); redisUtil.discard(); } }
2.從新寫登陸接口代碼,完整代碼以下:
/** * 手動事務工具類 */ @Autowired private RedisDataSoureceTransaction manualTransaction; @Override public BaseResponse<JSONObject> login(@RequestBody UserLoginInDTO userLoginInpDTO) { // 1.驗證參數 String mobile = userLoginInpDTO.getMobile(); if (StringUtils.isEmpty(mobile)) { return setResultError("手機號碼不能爲空!"); } String password = userLoginInpDTO.getPassword(); if (StringUtils.isEmpty(password)) { return setResultError("密碼不能爲空!"); } // 判斷登錄類型 String loginType = userLoginInpDTO.getLoginType(); if (StringUtils.isEmpty(loginType)) { return setResultError("登錄類型不能爲空!"); } // 目的是限制範圍 if (!(loginType.equals(Constants.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_IOS) || loginType.equals(Constants.MEMBER_LOGIN_TYPE_PC))) { return setResultError("登錄類型出現錯誤!"); } // 設備信息 String deviceInfor = userLoginInpDTO.getDeviceInfor(); if (StringUtils.isEmpty(deviceInfor)) { return setResultError("設備信息不能爲空!"); } // 2.對登錄密碼實現加密 String newPassWord = MD5Util.MD5(password); // 3.使用手機號碼+密碼查詢數據庫 ,判斷用戶是否存在 UserDo userDo = userMapper.login(mobile, newPassWord); if (userDo == null) { return setResultError("用戶名稱或者密碼錯誤!"); } TransactionStatus transactionStatus = null; try { // 1.獲取用戶UserId Long userId = userDo.getUserId(); // 2.生成用戶令牌Key String keyPrefix = Constants.MEMBER_TOKEN_KEYPREFIX + loginType; // 5.根據userId+loginType 查詢當前登錄類型帳號以前是否有登錄過,若是登錄過 清除以前redistoken UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType); transactionStatus = manualTransaction.begin(); // // ####開啓手動事務 if (userTokenDo != null) { // 若是登錄過 清除以前redistoken String oriToken = userTokenDo.getToken(); // 移除Token generateToken.removeToken(oriToken); int updateTokenAvailability = userTokenMapper.updateTokenAvailability(oriToken); if (updateTokenAvailability < 0) { manualTransaction.rollback(transactionStatus); return setResultError("系統錯誤"); } } // 4.將用戶生成的令牌插入到Token記錄表中 UserTokenDo userToken = new UserTokenDo(); userToken.setUserId(userId); userToken.setLoginType(userLoginInpDTO.getLoginType()); String newToken = generateToken.createToken(keyPrefix, userId + ""); userToken.setToken(newToken); userToken.setDeviceInfor(deviceInfor); int result = userTokenMapper.insertUserToken(userToken); if (!toDaoResult(result)) { manualTransaction.rollback(transactionStatus); return setResultError("系統錯誤!"); } // #######提交事務 JSONObject data = new JSONObject(); data.put("token", newToken); manualTransaction.commit(transactionStatus); return setResultSuccess(data); } catch (Exception e) { try { // 回滾事務 manualTransaction.rollback(transactionStatus); } catch (Exception e1) { } return setResultError("系統錯誤!"); } }
3.核心代碼:
DB/Redis插入 | DB/Redis更新 |
---|---|
![]() |
![]() |
提交 | 拋異常(主要捕獲Redis異常) |
---|---|
![]() |
![]() |
首先,能夠看到數據庫和Redis裏面都沒有內容:
數據庫內容 | Redis內容 |
---|---|
![]() |
![]() |
啓動會員項目後,使用swagger訪問登陸接口,斷點走過redis插入後,能夠看到Redis裏面沒有內容,由於事務尚未提交:
斷點位置 | Redis數據 |
---|---|
![]() |
![]() |
斷點繼續走到數據庫插入數據,能夠看到數據庫裏面仍是沒有內容,由於事務也沒有提交:
斷點位置 | 數據庫數據 |
---|---|
![]() |
![]() |
最後斷點走過提交,能夠看到,數據庫可Redis裏面均有內容了:
Redis | 數據庫 |
---|---|
![]() |
![]() |
本文主要講解了經過Redis事務與數據庫事務同步的方式,來保持數據狀態的一致性。