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;
}
}