您当前的位置: 首页 >  数据库

淘东电商项目(21) -Redis如何与数据库状态保持一致?

杨林伟 发布时间:2020-03-04 17:07:28 ,浏览量:3

引言

在上一节《淘东电商项目(20) -会员唯一登录》,主要讲解会员如何实现三端唯一登录。

本文代码已提交至Github(版本号:31112e64e8bc832a1416c2fcfd064b5e45b45f32),有兴趣的同学可以下载来看看:https://github.com/ylw-github/taodong-shop

本文讲解会员服务中数据库状态与Redis服务状态如何保持一致性。

本文目录结构: l____引言 l____ 1. 问题引出 l____ 2. 解决思路 l____ 3. 代码实现 l____ 4. 测试 l____ 5. 第三方框架推荐 l____总结

1. 问题引出

下面先来贴一下登录接口的代码:

@Override
public BaseResponse 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到数据库的时候失败了,如何解决呢?

这就是本文主要讲的内容了,Redis如何与数据库状态保持一致?

2. 解决思路

可以看到上面出现的问题,很容易让我们联想起“「事务」”,事务可以保持ACID,我们知道数据库是有事务的,Redis也有事务?那能否把这两者同时使用呢?比如如下场景:

  1. 如果redis更新操作失败时,数据库更新操作也要失败
  2. 如果数据库更新操作失败时,Redis更新操作也要失败

其实解决方案已经显露出来了,我们可以重写数据库的事务和Redis事务,把两者合成一种新的事务解决方案,满足:

  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 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             
关注
打赏
1688896170
查看更多评论
0.4362s