淘東電商項目(21) -Redis如何與數據庫狀態保持一致?

引言

在上一節《淘東電商項目(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

1. 問題引出

下面先來貼一下登陸接口的代碼:數據庫

@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

2. 解決思路

能夠看到上面出現的問題,很容易讓咱們聯想起「「事務」」,事務能夠保持ACID,咱們知道數據庫是有事務的,Redis也有事務?那可否把這二者同時使用呢?好比以下場景:框架

  1. 若是redis更新操做失敗時,數據庫更新操做也要失敗
  2. 若是數據庫更新操做失敗時,Redis更新操做也要失敗

其實解決方案已經顯露出來了,咱們能夠重寫數據庫的事務和Redis事務,把二者合成一種新的事務解決方案,知足:ide

  1. 數據庫事務開啓的同時,Redis事務也要開啓begin
  2. 數據庫事務提交的同時,Redis事務也要提交commit
  3. 數據庫事務回滾的同時,Redis事務也要回滾rollback

3. 代碼實現

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異常)
在這裏插入圖片描述 在這裏插入圖片描述

4. 測試

首先,能夠看到數據庫和Redis裏面都沒有內容:

數據庫內容 Redis內容
在這裏插入圖片描述 在這裏插入圖片描述

啓動會員項目後,使用swagger訪問登陸接口,斷點走過redis插入後,能夠看到Redis裏面沒有內容,由於事務尚未提交:

斷點位置 Redis數據
在這裏插入圖片描述 在這裏插入圖片描述

斷點繼續走到數據庫插入數據,能夠看到數據庫裏面仍是沒有內容,由於事務也沒有提交:

斷點位置 數據庫數據
在這裏插入圖片描述 在這裏插入圖片描述

最後斷點走過提交,能夠看到,數據庫可Redis裏面均有內容了:

Redis 數據庫
在這裏插入圖片描述 在這裏插入圖片描述

總結

本文主要講解了經過Redis事務與數據庫事務同步的方式,來保持數據狀態的一致性。

相關文章
相關標籤/搜索