在上一节《淘东电商项目(20) -会员惟一登陆》,主要讲解会员如何实现三端惟一登陆。java
本文代码已提交至Github(版本号:
31112e64e8bc832a1416c2fcfd064b5e45b45f32
),有兴趣的同窗能够下载来看看:https://github.com/ylw-github/taodong-shopgit
本文讲解会员服务中数据库状态与Redis服务状态如何保持一致性。github
本文目录结构:
l____引言
l____ 1. 问题引出
l____ 2. 解决思路
l____ 3. 代码实现
l____ 4. 测试
l____ 5. 第三方框架推荐
l____总结redis
下面先来贴一下登陆接口的代码:数据库
@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到数据库的时候失败了,如何解决呢?markdown
这就是本文主要讲的内容了,Redis如何与数据库状态保持一致?app
能够看到上面出现的问题,很容易让咱们联想起“「事务」”,事务能够保持ACID
,咱们知道数据库是有事务的,Redis也有事务?那可否把这二者同时使用呢?好比以下场景:框架
- 若是redis更新操做失败时,数据库更新操做也要失败
- 若是数据库更新操做失败时,Redis更新操做也要失败
其实解决方案已经显露出来了,咱们能够重写数据库的事务和Redis事务,把二者合成一种新的事务解决方案,知足:ide
begin
)commit
)rollback
)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异常) |
---|---|
![]() |
![]() |
首先,能够看到数据库和Redis里面都没有内容:
数据库内容 | Redis内容 |
---|---|
![]() |
![]() |
启动会员项目后,使用swagger访问登陆接口,断点走过redis插入后,能够看到Redis里面没有内容,由于事务尚未提交:
断点位置 | Redis数据 |
---|---|
![]() |
![]() |
断点继续走到数据库插入数据,能够看到数据库里面仍是没有内容,由于事务也没有提交:
断点位置 | 数据库数据 |
---|---|
![]() |
![]() |
最后断点走过提交,能够看到,数据库可Redis里面均有内容了:
Redis | 数据库 |
---|---|
![]() |
![]() |
本文主要讲解了经过Redis事务与数据库事务同步的方式,来保持数据状态的一致性。