引言
在上一节《淘东电商项目20 -会员唯一登录》主要讲解会员如何实现三端唯一登录。
本文代码已提交至Github版本号31112e64e8bc832a1416c2fcfd064b5e45b45f32有兴趣的同学可以下载来看看https://github.com/ylw-github/taodong-shop
本文讲解会员服务中数据库状态与Redis服务状态如何保持一致性。
本文目录结构 l____引言 l____ 1. 问题引出 l____ 2. 解决思路 l____ 3. 代码实现 l____ 4. 测试 l____ 5. 第三方框架推荐 l____总结
1. 问题引出
下面先来贴一下登录接口的代码
Overridepublic 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.获取useridLong userId userDo.getUserId();// 5.根据userIdloginType 查询当前登陆类型账号之前是否有登陆过如果登陆过 清除之前redistokenUserTokenDo userTokenDo userTokenMapper.selectByUserIdAndLoginType(userId, loginType);if (userTokenDo ! null) {// 如果登陆过 清除之前redistokenString token userTokenDo.getToken();Boolean isremoveToken generateToken.removeToken(token);if (isremoveToken) {// 把该token的状态改为1userTokenMapper.updateTokenAvailability(token);}}// .生成对应用户令牌存放在redis中String keyPrefix Constants.MEMBER_TOKEN_KEYPREFIX loginType;String newToken generateToken.createToken(keyPrefix, userId "");// 1.插入新的tokenUserTokenDo 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也有事务那能否把这两者同时使用呢比如如下场景
其实解决方案已经显露出来了我们可以重写数据库的事务和Redis事务把两者合成一种新的事务解决方案满足
3. 代码实现
1.先贴上数据库事务与Redis事务的合成工具类
/*** description: Redis与 DataSource 事务封装* create by: YangLinWei* create time: 2020/3/4 3:34 下午*/ComponentScope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)public class RedisDataSoureceTransaction {Autowiredprivate RedisUtil redisUtil;/*** 数据源事务管理器*/Autowiredprivate 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.重新写登录接口代码完整代码如下
/*** 手动事务工具类*/Autowiredprivate RedisDataSoureceTransaction manualTransaction;Overridepublic 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.获取用户UserIdLong userId userDo.getUserId();// 2.生成用户令牌KeyString keyPrefix Constants.MEMBER_TOKEN_KEYPREFIX loginType;// 5.根据userIdloginType 查询当前登陆类型账号之前是否有登陆过如果登陆过 清除之前redistokenUserTokenDo userTokenDo userTokenMapper.selectByUserIdAndLoginType(userId, loginType);transactionStatus manualTransaction.begin();// // ####开启手动事务if (userTokenDo ! null) {// 如果登陆过 清除之前redistokenString oriToken userTokenDo.getToken();// 移除TokengenerateToken.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事务与数据库事务同步的方式来保持数据状态的一致性。