《果然新鲜》电商项目(26)- Redis如何与数据库状态保持一致?

Posted IT老刘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《果然新鲜》电商项目(26)- Redis如何与数据库状态保持一致?相关的知识,希望对你有一定的参考价值。

引言

在上一节《果然新鲜电商项目(25)- 会员唯一登录》,主要讲解会员如何实现三端唯一登录。
本文讲解会员服务中数据库状态与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和数据库的操作是同步的,那如果插入TokenRedis成功了,但是插入Token到数据库的时候失败了,如何解决呢?

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

2.解决思路

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

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

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

  1. 数据库事务开启的同时,Redis事务也要开启(begin
  2. 数据库事务提交的同时,Redis事务也要提交(commit
  3. 数据库事务回滚的同时,Redis事务也要回滚(rollback

3.代码实现

  1. 先贴上数据库事务与Redis事务的合成工具类:
 <!-- mybatis相关依赖 -->
 <dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>1.1.1</version>
 </dependency>

 <!-- mysql 依赖 -->
 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.25</version>
 </dependency>
package com.guoranxinxian.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;

@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();
    



  1. 重新写登录接口代码,完整代码如下:
package com.guoranxinxian.impl;

import com.alibaba.fastjson.JSONObject;
import com.guoranxinxian.api.BaseResponse;
import com.guoranxinxian.constants.Constants;
import com.guoranxinxian.entity.BaseApiService;
import com.guoranxinxian.entity.UserDo;
import com.guoranxinxian.entity.UserTokenDo;
import com.guoranxinxian.mapper.UserMapper;
import com.guoranxinxian.mapper.UserTokenMapper;
import com.guoranxinxian.member.dto.input.UserLoginInDTO;
import com.guoranxinxian.service.MemberLoginService;
import com.guoranxinxian.util.GenerateToken;
import com.guoranxinxian.util.MD5Util;
import com.guoranxinxian.util.RedisDataSoureceTransaction;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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 javax.annotation.Resource;

@RestController
@Slf4j
public class MemberLoginServiceImpl extends BaseApiService<JSONObject> implements MemberLoginService 

    @Resource
    private UserMapper userMapper;

    @Autowired
    private GenerateToken generateToken;

    @Resource
    private UserTokenMapper userTokenMapper;

    /**
     * 手动事务工具类
     */
    @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("系统错误!");
        

    

4. 测试

首先,可以看到数据库和Redis里面都没有内容:

数据库内容

Redis内容

启动会员项目后,使用swagger访问登录接口,断点走过redis插入后,可以看到Redis里面没有内容,因为事务还没有提交:

断点位置

Redis数据

断点继续走到数据库插入数据,可以看到数据库里面还是没有内容,因为事务也没有提交:

数据库数据

最后断点走过提交,可以看到,数据库可Redis里面均有内容了:

Redis数据

数据库

5.总结

本文主要讲解了通过Redis事务与数据库事务同步的方式,来保持数据状态的一致性。

以上是关于《果然新鲜》电商项目(26)- Redis如何与数据库状态保持一致?的主要内容,如果未能解决你的问题,请参考以下文章

《果然新鲜》电商项目(19)- 公众号获取注册码功能

《果然新鲜》电商项目(23)- 全局异常捕获

《果然新鲜》电商项目(22)- DTO接口细分

《果然新鲜》电商项目(24)- 日志打印

《果然新鲜》电商项目(12)- Maven私服仓库使用

《果然新鲜》电商项目(25)- 会员唯一登录