1.數據不一致的業務場景:
以唯一登錄爲業務場景:在移除令牌成功後,變更令牌可用狀態時出現錯誤導致令牌狀態未變更,那麼Redis中已經不存在此用戶的令牌,而Mysql中存儲的上一個用戶的令牌狀態爲可用。那麼就會出現這樣的情況:
用戶帶着令牌來訪問時由於Redis中不存在就無法訪問,於是兩個用戶均無法訪問。一個新的用戶密碼校驗成功後發現這個用戶登錄過,但是Redis移除出現異常,因爲Redis中沒有該用戶的Redis,數據的不一致造成該用戶無法登錄的後果。
2.Redis與Mysql同步事務
package com.tx.servicemember.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.tx.base.BaseApiService;
import com.tx.base.BaseResponse;
import com.tx.constants.Constant;
import com.tx.servicemember.token.GenerateToken;
import com.tx.core.utils.MD5Util;
import com.tx.apimember.service.MemberLoginService;
import com.tx.memberdto.input.dto.UserLoginInpDTO;
import com.tx.servicemember.mapper.UserMapper;
import com.tx.servicemember.mapper.UserTokenMapper;
import com.tx.servicemember.mapper.entity.UserDo;
import com.tx.servicemember.mapper.entity.UserTokenDo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.tx.servicemember.utils.RedisUtil;
import com.tx.servicemember.utils.RedisDataSoureceTransaction;
@RestController
public class MemberLoginServiceImpl extends BaseApiService<JSONObject> implements MemberLoginService {
@Autowired
private UserMapper userMapper;
@Autowired
private GenerateToken generateToken;
@Autowired
private UserTokenMapper userTokenMapper;
/**
* 手動事務工具類
*/
@Autowired
private RedisDataSoureceTransaction manualTransaction;
/**
* redis 工具類
*/
@Autowired
private RedisUtil redisUtil;
@Override
public BaseResponse<JSONObject> login(@RequestBody UserLoginInpDTO 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(Constant.MEMBER_LOGIN_TYPE_ANDROID) || loginType.equals(Constant.MEMBER_LOGIN_TYPE_IOS)
|| loginType.equals(Constant.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
TransactionStatus transactionStatus = null;
try{
// 4.獲取userid
Long userId = userDo.getUserId();
// 5.根據userId+loginType 查詢當前登陸類型賬號之前是否有登陸過,如果登陸過 清除之前redistoken
UserTokenDo userTokenDo = userTokenMapper.selectByUserIdAndLoginType(userId, loginType);
transactionStatus = manualTransaction.begin();
// // ####開啓手動事務
if (userTokenDo != null) {
// 如果登陸過 清除之前redistoken
String token = userTokenDo.getToken();
generateToken.removeToken(token);
// 把該token的狀態改爲1
int updateTokenAvailability = userTokenMapper.updateTokenAvailability(token);
if (updateTokenAvailability < 0) {
manualTransaction.rollback(transactionStatus);
return setResultError("系統錯誤");
}
}
// .生成對應用戶令牌存放在redis中
String keyPrefix = Constant.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);
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("系統錯誤!");
}
}
// 查詢用戶信息的話如何實現? redis 與數據庫如何保證一致問題
}
package com.tx.servicemember.utils;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
@Component
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public class RedisDataSoureceTransaction {
@Autowired
private RedisUtil redisUtil;
/**
* 數據源事務管理器
*/
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
/**
* 開始事務 採用默認傳播行爲
*
* @return
*/
public TransactionStatus begin() {
// 手動begin數據庫事務
// 1.開啓數據庫的事務 事務傳播行爲
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
// 2.開啓redis事務
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);
}
/**
* 回滾事務
*
* @param transactionStatus
* @throws Exception
*/
public void rollback(TransactionStatus transactionStatus) throws Exception {
if (transactionStatus == null) {
throw new Exception("transactionStatus is null");
}
// 1.回滾數據庫事務 redis事務和數據庫的事務同時回滾
dataSourceTransactionManager.rollback(transactionStatus);
// // 2.回滾redis事務
// redisUtil.discard();
}
// 如果redis的值與數據庫的值保持不一致話
}
package com.tx.servicemember.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/***
* @Author Sunny
* @Description //TODO Redis工具類
* @Date 11:14 2019/9/17 * @Param
* @return
*/
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 存放string類型
*
* @param key
* key
* @param data
* 數據
* @param timeout
* 超時間
*/
public void setString(String key, String data, Long timeout) {
try {
stringRedisTemplate.opsForValue().set(key, data);
if (timeout != null) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
} catch (Exception e) {
}
}
/**
* 開啓Redis 事務
*
* @param isTransaction
*/
public void begin() {
// 開啓Redis 事務權限
stringRedisTemplate.setEnableTransactionSupport(true);
// 開啓事務
stringRedisTemplate.multi();
}
/**
* 提交事務
*
* @param isTransaction
*/
public void exec() {
// 成功提交事務
stringRedisTemplate.exec();
}
/**
* 回滾Redis 事務
*/
public void discard() {
stringRedisTemplate.discard();
}
/**
* 存放string類型
*
* @param key key
* @param data 數據
*/
public void setString(String key, String data) {
setString(key, data, null);
}
/**
* 根據key查詢string類型
*
* @param key
* @return
*/
public String getString(String key) {
String value = stringRedisTemplate.opsForValue().get(key);
return value;
}
/**
* 根據對應的key刪除key
*
* @param key
*/
public Boolean delKey(String key) {
return stringRedisTemplate.delete(key);
}
}
package com.tx.base;
import com.tx.constants.Constant;
import lombok.Data;
import org.springframework.stereotype.Component;
/***
* @Author Sunny
* @Description //TODO 微服務接口實現該接口可以使用傳遞參數可以直接封裝統一返回結果集
* @Date 11:14 2019/9/17
* @Param
* @return
*/
@Data
@Component
public class BaseApiService<T> {
public BaseResponse<T> setResultError(Integer code, String msg) {
return setResult(code, msg, null);
}
// 返回錯誤,可以傳msg
public BaseResponse<T> setResultError(String msg) {
return setResult(Constant.HTTP_RES_CODE_500, msg, null);
}
// 返回成功,可以傳data值
public BaseResponse<T> setResultSuccess(T data) {
return setResult(Constant.HTTP_RES_CODE_200, Constant.HTTP_RES_CODE_200_VALUE, data);
}
// 返回成功,沒有data值
public BaseResponse<T> setResultSuccess() {
return setResult(Constant.HTTP_RES_CODE_200, Constant.HTTP_RES_CODE_200_VALUE, null);
}
// 返回成功,沒有data值
public BaseResponse<T> setResultSuccess(String msg) {
return setResult(Constant.HTTP_RES_CODE_200, msg, null);
}
// 通用封裝
public BaseResponse<T> setResult(Integer code, String msg, T data) {
return new BaseResponse<T>(code, msg, data);
}
// 調用數據庫層判斷
public Boolean toDaoResult(int result) {
return result > 0 ? true : false;
}
}