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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 淘东电商项目(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事务与数据库事务同步的方式,来保持数据状态的一致性。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
1月前
|
存储 关系型数据库 MySQL
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB区别,适用场景
一个项目用5款数据库?MySQL、PostgreSQL、ClickHouse、MongoDB——特点、性能、扩展性、安全性、适用场景比较
|
26天前
|
SQL JavaScript 关系型数据库
node博客小项目:接口开发、连接mysql数据库
【10月更文挑战第14天】node博客小项目:接口开发、连接mysql数据库
|
1月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
54 11
|
2月前
|
JavaScript Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
本文介绍了一个基于Spring Boot和Vue.js实现的在线考试系统。随着在线教育的发展,在线考试系统的重要性日益凸显。该系统不仅能提高教学效率,减轻教师负担,还为学生提供了灵活便捷的考试方式。技术栈包括Spring Boot、Vue.js、Element-UI等,支持多种角色登录,具备考试管理、题库管理、成绩查询等功能。系统采用前后端分离架构,具备高性能和扩展性,未来可进一步优化并引入AI技术提升智能化水平。
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
|
2月前
|
Java 关系型数据库 MySQL
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术的房屋租赁系统,旨在通过自动化和信息化手段提升房屋管理效率,优化租户体验。系统采用JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Layui和Spring Boot 2.0等技术栈,实现了高效的房源管理和便捷的租户服务。通过该系统,房东可以轻松管理房源,租户可以快速找到合适的住所,双方都能享受数字化带来的便利。未来,系统将持续优化升级,提供更多完善的服务。
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
|
1月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
181 10
|
1月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
|
1月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
43 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
Oracle NoSQL 关系型数据库
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
392 2