您好,欢迎访问一九零五行业门户网

Redis+SpringBoot案例分析

一、项目环境前端技术栈:vue-cli
前端软体:webstorm 2020.3
前端样式: bootstrap
后端技术栈:springboot
后端软体:intellij ieda2019
javajdk:1.8
服务器:阿里云centos 7
其他:mybatis,redis,mysql,docker,shiro
二、项目演示项目源码:shoppingproject01_pub : version6.0
项目参考:project05;不良人_vue-cli;不良人_redis;不良人_axios;尚硅谷_redis
项目功能:
1)邮箱注册登录:
用户应用邮箱注册点击提交后,网站会给用户发送邮件附带激活码链接,用户点击链接实现账号激活传送门。
2)短信注册登录:
用户应用手机号注册时,点击“获取验证码”按钮,手机会接收到网站发送的短信附带验证码。基于redis实现了验证码有效期5分钟,每个手机号只能获取三次短信验证码传送门。
3)alipay支付:
通过下载android版支付宝沙箱app用户可通过alipay扫码购买网站上的商品,后台mysql会记录该订单行为传送门,网页展示如图1所示。
图1 商品展示页面4)用户小分级:
当用户扫码购买年度vip会员后,购买网站上的商品一律半价,后台mysql记录用户角色的变更。
5)用户积分排行榜:
用户购买商品会增加自己的积分,网页展示如图2所示。
图2 排行榜展示页面项目中遇到的大坑:
1)邮件发送功能本地测试通过,服务器端测试bug频出,解决办法。
2)项目在服务器部署后,无法连接服务器上的redis。解决办法:(1)将redis在服务器部署而非docker;(2)将redis端口改为7000;(3)防火墙active状态下放行服务器和阿里云的7000端口;(4)修改redis.conf文件。
3)git上传本地源码到gitee,误操作导致本地源码被gitee上的旧代码覆盖,第二天才发现。解决办法:因为在服务器上留有源码的jar包,通过反编译工具jd_gui救回半条命。另外git上传文件参考传送门。
三、主要模块说明1 vue-cli模块说明:
1.1 vue-cli的概述:
1)以前后端分离、单页面web应用(spa)为特点,vue-cli可以创建一个vue项目,这个项目存在脚手架规范。vue-cli的优势如下:
(1) 基于脚手架规范的开发会变得很灵活。
(2) vue-cli基于webpack构建并带有合理的默认配置,打包工具webpack能够聚合单页面和各种开发组件。
(3) vue-cli是一个丰富的官方插件集合,继承了前端生态中最好的工具。
2)安装过程:
(1) 安装webstorm(用于开发),安装node.js,安装vue-cli,安装axios(用于发起跨域请求),引入bootstrap样式。
3)部署过程:
npm run build # 在webstorm终端执行,生成dist文件夹docker pull nginx:1.19.10 # 不建议vue-cli项目部署到tomcat,因为tomcat属于动态服务器,启动需要java环境,是为了解析动态语言jsp的;像纯静态的就部署到静态服务器nginx上。mkdir html # 为了做docker容器内外的数据卷映射mv dist/ html/docker run -p 80:80 --name nginx01 -d -v /root/html/dist/:/usr/share/nginx/html nginx:1.19.10 # 数据卷映射# 此时可访问 http://120.79.133.235:80/index.html
4)vue-cli开发要点:
(1)在webstorm中,开发过程主要面向src文件,如图3所示:
图3 webstorm目录
[1] 首先掌握路由(router)和组件(components[公有组件],views[私有组件]),组件就是一个个“页面”,组件建立后要向路由进行注册; [2] asserts封装了bootstrap样式,并在main.js中被导入; [3] 为了发送跨域请求,在utils中封装了axios实例,代码如下:
import axios from 'axios'// 创建默认实例const instance = axios.create({ baseurl: 'http://120.79.133.235:8989/eb', // timeout: 10000,});// 请求拦截器instance.interceptors.request.use(config=>{ console.log("请求拦截器"); return config;})// 响应拦截器instance.interceptors.response.use(response=>{ console.log("响应拦截器"); return response;}, err=>{ console.log("响应出现错误时进入的拦截器");});// 暴露instance实例对象export default instance;
在各组件中,对于后端的get和post请求方法如下:
// get请求// 向后端接口发当前页码,获取当前页面的商品listinstance.get("/item/findallitem?page="+this.page).then(res=>{ that.items = res.data.items; that.totalpage = res.data.totalpage; that.page = res.data.page; });// post请求// 向后端接口发送当前商品id和用户id,获取商品购买状态instance.post("/order/alipay/callback",{itemid:this.itemid,userid:this.user.id}).then(res=>{ if ( res.data.code == 20000 ) { alert("提示:您已购买该商品"); } else { alert("提示:您未购买该商品"); } }); }
[4] 组件之间的跳转和传值方法如下:
// 跳转到mailreg组件this.$router.push({name:"mailreg"});// 跳转到item组件,并传递当前商品的idthis.$router.push({path:"/item",query:{itemid:myid}});// item组件接收方法:this.itemid = this.$route.query.itemid;// 另外不同组件可以依据token获取登录用户信息,需要用到redis,详见下文
2 用户积分排行榜模块说明:
1.1 reids概述:
1) redis是一种基于内存的数据存储nosql;
2) redis支持丰富的数据类型(string, list, set, zset, hash);
3) redis有两种持久化方法: (1)快照(snapshot)存储,也叫rdb持久化,保存当前时刻的数据状态;(2) aof(append only file)存储,将所有redis的写命令记录到日志文件中,redis支持持久化间隔最快也是一秒,所以它是事务不安全的,即是可能丢失数据的。
4)redis的应用场景:
(1) 利用redis字符串完成项目中手机验证码存储的实现。------本项目采用
(2) 利用redis字符串类型完成具有时效性的业务功能,如订单还有40分钟即将关闭。
(3) 利用redis实现分布式集群系统中的session共享。
(4) 利用redis的zset数据类型(可排序set类型:元素+分数)实现排行榜功能。 ------本项目采用
(5) 利用redis完成分布式缓存。 ------本项目实现mysql中数据的缓存
(6) 利用redis存储认证之后的token信息。 ------非常方便,本项目采用。
(7) 利用redis解决分布式集群系统中分布式锁问题。
1.2 基于redis实现前端组件从后端获取用户信息:
step1:前端login.vue组件中用户输入登录信息提交的接口如下:
// 这里调用了后端/user/login接口,获取当前登录用户的token,存入session的localstorage中,在后续网页浏览过程中可随时调取这个tokeninstance.post("/user/login",this.user).then(res=>{ if ( res.data.state ) { alert(res.data.msg+",点击确定进入主页"); // 前端存储token信息 localstorage.setitem("token",res.data.token); that.$router.push({path:"/itemlist"}); } else { alert(res.data.msg); that.user = {}; } });
step2:后端/user/login接口实现如下:
// controller层@postmapping("login")public map<string, object> loginaccount(@requestbody user user, httpsession session) { return userservice.loginaccount(user, session);}// service层// 情况3:查询到一个用户时// 获取主体对象try { subject subject = securityutils.getsubject(); subject.login(new usernamepasswordtoken(user.getname(), user.getpassword())); user userdb = userlistdb.get(0); string token = session.getid(); if (userdb.getscore() == null) { userdb.setscore(0.0); userdao.updateuserscore(userdb); } redistemplate.opsforvalue().set("token_" + token, userdb, 30, timeunit.minutes); redistemplate.opsforzset().add("userrank", userdb, userdb.getscore()); map.put("token", token); map.put("state",true); map.put("msg","登录成功"); return map; ...
redis整合springboot有两种template,即redistemplate和stringredistemplate。其中stringredistemplate是redistemplate的子类,两个方法基本一致,不同之处在于操作的数据类型不同,redistemplate中的两个泛型都是object,意味着存储的key和value都可以是一个对象,而stringredistemplate的两个泛型都是string,意味着stringredistemplate的key和value都只能是字符串。
在step2中,我将token和数据库中的用户信息userdb绑定在一起存入了redis中,后续前端组件获取登录用户信息的代码如下:
// 从localstorage获取tokenlet token = localstorage.getitem("token");let that = this;// 发送axios请求,根据token获取用户信息instance.get("/user/token?token="+token).then(res=>{that.user = res.data;console.log(that.user);})
后端/user/token的接口如下:
@getmapping({"token"})public user finduser(string token) { system.out.println("接收的token信息:" + token); return (user)redistemplate.opsforvalue().get("token_" + token);}
step3:用户退出登录时,应消除浏览器中对应的token,后端接口代码如下:
// 退出登录 @deletemapping({"logout"}) public map<string, object> logout(string token) { map<string, object> map = new hashmap<>(); try { redistemplate.delete("token_" + token); subject subject = securityutils.getsubject(); subject.logout(); map.put("state", true); map.put("msg", "提示:退出账户成功"); return map; } catch (exception e) { e.printstacktrace(); map.put("state", false); map.put("msg", "提示:退出账户失败"); return map; } }
1.3 基于redis的用户积分排行榜实现:
mysql中的用户信息如图4所示:
图4 mysql中的用户信息
redis中的userrank如图5所示:
图5 redis中的userrankstep1:当用户登录时,他的首要任务是接入userrank对应的信息,后端代码如下:
if (userdb.getscore() == null) { userdb.setscore(0.0); userdao.updateuserscore(userdb);} redistemplate.opsforvalue().set("token_" + token, userdb, 30, timeunit.minutes); redistemplate.opsforzset().add("userrank", userdb, userdb.getscore());
userdb是数据库中当前登录用户的信息(一定是有的,你注册了,对吧?),若用户首次登录我将他的分数在数据库设置为0.0,之后我在redis的zset中加入这个用户,你知道,set集合不会存储重复key值的元素,因此不会同一个用户出现在userrank中两次。两个template完成了token绑定user,user绑定userrank中score的过程,之后的分数更新过程会反复使用这两个template实现。
step2:当用户信息更新时,相应的与用户信息有关的两个template都要发生变化,代码如下:
// key值序列化redistemplate.setkeyserializer(new stringredisserializer());// 由当前用户的token获取当前用户的信息user firstuser = (user)redistemplate.opsforvalue().get("token_" + token);// 删除zset中的当前用户 redistemplate.opsforzset().remove("userrank", firstuser);// 产生新的当前用户(昵称改变)list<user> userlistdb = this.userdao.selectuserbyname(user.getname());user seconduser = userlistdb.get(0);// 更新token中当前用户的信息redistemplate.opsforvalue().set("token_" + token, seconduser, 30, timeunit.minutes);// 产生zset中的当前用户redistemplate.opsforzset().add("userrank", seconduser, seconduser.getscore());
step3:当用户扫码支付时,首次进入的后端controller如下:
// 支付单件商品 @getmapping("payforitem") public byte[] alipay(string itemid,string userid, string token) { this.token = token; log.info("itemid=====>"+itemid); log.info("userid=====>"+userid); payvo payvo = new payvo(); payvo.setitemid(itemid); payvo.setuserid(userid); system.out.println(payvo.getuserid()); return alipayservice.alipay(payvo); }
在alipayservice有一个小型用户分级,即vip用户购物价格减半:
// 1:支付的用户 string userid = payvo.getuserid(); // my 1: 依据用户id查询用户 user user = userservice.selectuserbyid(integer.valueof(userid)); // 依据商品id查询商品 item item = itemservice.getitembyid(payvo.getitemid()); // my 1: 依据用户id查询用户 if ( item == null ) return null; // 2: 支付金额 string tempmoney = item.getprice().tostring(); string money = ""; if ( user.getrole().equals("normal") ) { money = tempmoney; } if ( user.getrole().equals("vip") ) { double tempmoney2 = double.valueof(tempmoney)*0.5; money = string.valueof(tempmoney2); }
在payforitem相同文件下,调用了paycommonservice,在这里会实现用户积分更新和用户等级更新:
paycommonservice.payuserpublic(bodyjsonobject, userid, user.getname(), ordernumber, tradeno, "alipay", this.token);
将"vip"这件商品的id设置为“666”,当用户购买该商品时,当前用户更新过程如下:
if ( itemid.equals("666") ) { int myuserid = integer.valueof(userid); user userdb = userservice.selectuserbyid(myuserid); // key值序列化 this.redistemplate.setkeyserializer(new stringredisserializer()); // 由当前token获取当前用户信息 user firstuser = (user)redistemplate.opsforvalue().get("token_" + token); // 由当前用户信息删除当前用户zset redistemplate.opsforzset().remove("userrank", firstuser); // 更新当前用户信息身份 userdb.setrole("vip"); // 更新当前用户新身份的分数 userservice.updateuserrole(userdb); list<user> userlistdb = this.userdao.selectuserbyname(userdb.getname()); // 获取当前新身份用户的完整信息 user seconduser = userlistdb.get(0); // 更新当前token对应的当前用户 redistemplate.opsforvalue().set("token_" + token, seconduser, 30, timeunit.minutes); // 设置当前用户的zset redistemplate.opsforzset().add("userrank", seconduser, seconduser.getscore().doublevalue()); }
当前用户积分更新过程如下:
// 更新当前用户的积分 double tempscore = double.valueof(orderdetail.getprice()) * 0.3; string key1 = "token_" + token; // 由当前token获取当前用户 user user = (user)redistemplate.opsforvalue().get(key1); // 更新当前用户的zset分数 redistemplate.opsforzset().incrementscore("userrank", user, tempscore); // 获取当前用户的zset分数 double newscore = redistemplate.opsforzset().score("userrank", user); // 删除当前用户的zset(因为要更新当前用户的信息,将当前用户在数据库中的分数进行同步) redistemplate.opsforzset().remove("userrank", new object[] { user }); user.setscore(newscore); userdao.updateuserscore(user); // 更新token对应的当前用户的信息 redistemplate.opsforvalue().set(key1, user); // 新增当前用户的zset redistemplate.opsforzset().add("userrank", user, newscore);
以上就是redis+springboot案例分析的详细内容。
其它类似信息

推荐信息