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

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

引言

在上一节《淘东电商项目(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<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到数据库的时候失败了,如何解决呢?

这就是本文主要讲的内容了,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<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事务与数据库事务同步的方式,来保持数据状态的一致性。

目录
相关文章
|
8月前
|
存储 NoSQL Redis
阿里云高性能数据库Tair(兼容 Redis)收费价格,稳定可靠成本低
阿里云高性能云数据库Tair兼容Redis,提供Redis开源版和Tair企业版,支持多种存储介质与灵活扩展,适用于高并发场景。Tair具备亚毫秒级稳定延迟,保障业务连续性。价格方面,Redis开源版年费从72元起,Tair企业版年费从1224元起,具体费用根据配置不同有所变化。
|
存储 NoSQL 数据库
Redis 逻辑数据库与集群模式详解
Redis 是高性能内存键值数据库,广泛用于缓存与实时数据处理。本文深入解析 Redis 逻辑数据库与集群模式:逻辑数据库提供16个独立存储空间,适合小规模隔离;集群模式通过分布式架构支持高并发和大数据量,但仅支持 database 0。文章对比两者特性,讲解配置与实践注意事项,并探讨持久化及性能优化策略,助你根据需求选择最佳方案。
1000 5
|
NoSQL 安全 测试技术
Redis游戏积分排行榜项目中通义灵码的应用实战
Redis游戏积分排行榜项目中通义灵码的应用实战
491 4
|
10月前
|
存储 缓存 NoSQL
Redis 核心知识与项目实践解析
本文围绕 Redis 展开,涵盖其在项目中的应用(热点数据缓存、存储业务数据、实现分布式锁)、基础数据类型(string 等 5 种)、持久化策略(RDB、AOF 及混合持久化)、过期策略(惰性 + 定期删除)、淘汰策略(8 种分类)。 还介绍了集群方案(主从复制、哨兵、Cluster 分片)及主从同步机制,分片集群数据存储的哈希槽算法。对比了 Redis 与 Memcached 的区别,说明了内存用完的情况及与 MySQL 数据一致性的保证方案。 此外,详解了缓存穿透、击穿、雪崩的概念及解决办法,如何保证 Redis 中是热点数据,Redis 分布式锁的实现及问题解决,以及项目中分布式锁
279 1
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
302 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
缓存 NoSQL Redis
Redis原理—2.单机数据库的实现
本文概述了Redis数据库的核心结构和操作机制。
Redis原理—2.单机数据库的实现
|
NoSQL Java Redis
Redis Pipeline介绍 ---- 提高操作Redis数据库的执行效率。
Redis Pipeline是提高Redis执行效率的重要技术,通过批量发送命令,显著减少了网络往返次数,提高了系统的吞吐量和性能。在实际应用中,合理使用Pipeline可以有效优化Redis的性能,特别是在需要批量操作的场景下。本文通过Python和Java的示例代码展示了如何实现和使用Redis Pipeline,为开发者提供了具体的操作指南。
611 16
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
基于springboot+thymeleaf+Redis仿知乎网站问答项目源码
404 36
|
消息中间件 缓存 NoSQL
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
949 26