项目环境搭建
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nn18HsrO-1664517455146)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\目标.png)]
导入基础包:spring web, lombok, thymeleaf, mybatis-puls, mysqlDriver
<dependencies>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
</dependencies>
登录
数据表建立 id nickname password
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6w9vPoq-1664517455147)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\数据库.png)]
数据源配置
#配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/seckill
spring.datasource.username=root
spring.datasource.password=123456
添加sping-boot-security依赖
<!--ctrl+shift+'/'为快捷注释,spring-boot-security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置spring-security
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/**").hasRole("user")
.and()
.formLogin();
}
}
用户实体
@Data
public class User {
int id;
String nickname;
String password;
String roles;
}
根据name查询用户的mapper
@Mapper
public interface SelectUserByName {
@Select("select * from user where name=#{username}")
UserData SelectUserByName(String username);
}
用户验证service
@Service
public class UserAuthService implements UserDetailsService {
@Resource
SelectUserByName selectUserByName;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserData userData = selectUserByName.SelectUserByName(username);
//不存在用户
if(userData==null) throw new UsernameNotFoundException("未找到用户!");
//存在用户,则返回User(security.core.userdetails.User)形式用户
return User
.withUsername(userData.getNickname())
.password(userData.getPassword())
.roles(userData.getRoles())
.build();
}
}
配置Spring-security中传入参数为AuthenticationManagerBuilder auth的configure。BCryptPasswordEncoder()是spring中一种加密方式,主要计算hash值,该过程不可逆。只有将前端获取的密码进行加密,与使用相同方法加密存储在数据库的hash值匹配。
@Resource
UserAuthService userAuthService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userAuthService)
.passwordEncoder(new BCryptPasswordEncoder());//会对输入的密码进行加密,与数据库中的密码匹配,因此存入的密码也应是相同加密后的密码
}
配置静态文件路径
#配置静态资源
spring.mvc.static-path-pattern=/static/**
开放静态资源权限
.antMatchers("/static/**").permitAll()
关闭crsf(跨站域名伪造)
.csrf().disable();
完成登录映射
<form action="/doLogin" method="post">
.loginProcessingUrl("/doLogin").permitAll()
分布式会话
cookie和session原理:cookie是保存在浏览器的key-value,特点是每次请求cookie会发送给服务端。当浏览器被黑后,cookie中保存的数据便会泄露。session是第一次用户名、密码验证成功后保存在客户端的会话,具有唯一的sessionid及其属性,访问后会将sessionid及其属性存为cookie。
秒杀页面
商品列表
数据库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3HQeyQS-1664517455147)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品数据库.png)]
商品实体类
@Data
public class Goods {
int id;
String goods_name;
int price;
int stock;
Date begin_time;
Date end_time;
}
mapper
@Mapper
public interface SelectGoods {
@Results({
@Result(column = "id", property = "id"),
@Result(column = "goods_name", property = "goods_name"),
@Result(column = "price", property = "price"),
@Result(column = "stock", property = "stock"),
@Result(column = "begin_time", property = "begin_time"),
@Result(column = "end_time", property = "end_time")
})
@Select("select * from goods")
List<Goods> SelectGoods();
}
service
@Service
public class GoodsService {
@Resource
SelectGoods selectGoods;
public List<Goods> selectGoodsList(){
List<Goods> goodsList = selectGoods.SelectGoods();
return goodsList;
}
}
controller
@RequestMapping("/index")
public String index(Model model){
model.addAttribute("goodsList", goodsService.selectGoodsList());
System.out.println(goodsService.selectGoodsList());
return "index";
}
html
<div class="table-responsive">
<table class="table table-styled mb-0">
<thead>
<tr>
<th>id</th>
<th>名称</th>
<th>价格</th>
<th>库存</th>
<th>开始日期</th>
<th>结束日期</th>
</tr>
</thead>
<tbody>
<tr th:each="goods:${goodsList}">
<td th:text="${goods.getId()}">#JH2033</td>
<td th:text="${goods.getGoods_name()}">#JH2033</td>
<td th:text="${goods.getPrice()}">#JH2033</td>
<td th:text="${goods.getStock()}">#JH2033</td>
<td th:text="${#dates.format(goods.getBegin_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
<td th:text="${#dates.format(goods.getEnd_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
</tr>
</tbody>
</table>
</div>
结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ep95Kwlw-1664517455148)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品列表.png)]
商品详情
点击“详情“进行页面跳转,设置标签并传入商品id
<a th:href="@{/details(id=${goods.getId()})}">详情</a>
根据id进行商品查询
@Mapper
public interface SelectGoodsById {
@Results({
@Result(column = "id", property = "id"),
@Result(column = "goods_name", property = "goods_name"),
@Result(column = "price", property = "price"),
@Result(column = "stock", property = "stock"),
@Result(column = "begin_time", property = "begin_time"),
@Result(column = "end_time", property = "end_time")
})
@Select("select * from goods where id=#{id}")
Goods SelectGoodsById(int id);
}
public Goods selectGoodsDetails(int id){
Goods goods = selectGoodsById.SelectGoodsById(id);
return goods;
}
接受参数id并跳转到详情页面
@RequestMapping(value = "/details", method = RequestMethod.GET)
public String Details(Model model, @RequestParam("id") int goodsId)
详情页面
<td th:text="${goods.getId()}">#JH2033</td>
<td th:text="${goods.getGoods_name()}">#JH2033</td>
<td th:text="${goods.getPrice()}">#JH2033</td>
<td th:text="${goods.getStock()}">#JH2033</td>
<td th:text="${#dates.format(goods.getBegin_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
<td th:text="${#dates.format(goods.getEnd_time(), 'yyyy-MM-dd hh:mm:ss')}">#JH2033</td>
详情页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55y0W2QB-1664517455149)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\商品详情.png)]
秒杀倒计时
设置秒杀状态:>0距离秒杀的时间;=0秒杀进行中;-1秒杀已经结束
@RequestMapping(value = "/details", method = RequestMethod.GET)
public String Details(Model model, @RequestParam("id") int goodsId){
Goods goods = goodsService.selectGoodsDetails(goodsId);
//状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
int secKillStatus = 0;
Date nowDate = new Date();
//秒杀未开始
if (goods.getBegin_time().after(nowDate)) {
secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
} else if (goods.getEnd_time().before(nowDate)) {
secKillStatus=-1;
}
model.addAttribute("goods", goods);
model.addAttribute("secKillStatus", secKillStatus);
return "details";
}
页面根据不同的状态进行显示
<td>
<label class="mb-0 badge badge-primary" title="" data-original-title="Pending" th:if="${secKillStatus gt 0}">倒计时:<span th:text="${secKillStatus}"></span>秒</label>
<label class="mb-0 badge badge-success" title="" data-original-title="Pending" th:if="${secKillStatus eq 0}">秒杀进行中</label>
<label class="mb-0 badge badge-primary" title="" data-original-title="Pending" th:if="${secKillStatus eq -1}">已结束</label>
</td>
秒杀未开始时动态刷新倒计时,计时完毕后跳转到秒杀进行。利用js设置定时器动态刷新倒计时。
给页面中需要用的值设置id,js才能调用(大坑!,可以通过console.log(变量名)不断在浏览器控制台查看变量值调试)
<td>
<!--必须加入id="",才能被js获取-->
<input th:type="hidden" th:id="secKillStatus" th:value="${secKillStatus}">
<label th:if="${secKillStatus gt 0}">倒计时:<span id="countDown" th:text="${secKillStatus}"></span>秒</label>
<label th:if="${secKillStatus eq 0}">秒杀进行中</label>
<label th:if="${secKillStatus eq -1}">已结束</label>
</td>
设置一个定时器,每隔1s执行一次
<!--定时器代码-->
<script >
$(function (){
countDown();
});
//cuntDown中继续调用cuntDown实现秒数递减
function countDown(){
var remainSeconds = $("#secKillStatus").val();
var timeout;//定时器
//秒杀未开始
if(remainSeconds>0){
timeout = setTimeout(function (){
//将记录的状态值减去1
$("#secKillStatus").val(remainSeconds-1);
//将展示的值减1
$("#countDown").text(remainSeconds-1);
//继续调用
countDown();
},1000);
}
//如果倒计时定时器还存在,就清除定时器
else if(remainSeconds==0){
clearTimeout(timeout);
}
};
</script>
倒计时结束会停在0S,使用.html切换到“秒杀进行中”
<!--添加id用于js切换状态-->
<td th:id="secKillTip">
<!--必须加入id="",才能被js获取-->
<input th:type="hidden" th:id="secKillStatus" th:value="${secKillStatus}">
<label th:if="${secKillStatus gt 0}">倒计时:<span id="countDown" th:text="${secKillStatus}"></span>秒</label>
<label th:if="${secKillStatus eq 0}">秒杀进行中</label>
<label th:if="${secKillStatus eq -1}">已结束</label>
</td>
<!--定时器代码-->
<script >
$(function (){
countDown();
});
//cuntDown中继续调用cuntDown实现秒数递减
function countDown(){
var remainSeconds = $("#secKillStatus").val();
var timeout;//定时器
//秒杀未开始
if(remainSeconds>0){
timeout = setTimeout(function (){
//将记录的状态值减去1
$("#secKillStatus").val(remainSeconds-1);
//将展示的值减1
$("#countDown").text(remainSeconds-1);
//继续调用
countDown();
},1000);
}
//如果倒计时定时器还存在,就清除定时器
else if(remainSeconds==0){
clearTimeout(timeout);
$("#secKillTip").html("秒杀进行中")
}
else {
$("#secKillTip").html("已结束")
}
};
</script>
秒杀按钮
添加抢购按键,通过id可以操作按钮是否可用。
传回商品id进行后续操作
<td>
<form action="/doSekill" method="post">
<!--抢购时需要传回商品ID-->
<input type="hidden" name="goodsId" th:value="${goods.getId()}">
<!--必须设置为btn才能禁用时变灰色 按钮为蓝色,充满一个区块,通过id控制是否可用-->
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即抢购</button>
</form>
</td>
在js代码中,调整按钮。在“秒杀进行中”状态时,秒杀按钮设置为可用,其余时候为不可用。
//秒杀按钮可用
$("#buyButton").attr("disabled", false);
//秒杀按钮置灰
$("#buyButton").attr("disabled", true);
页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZNTVmdP9-1664517455150)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\抢购页面.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dhEHQToY-1664517455151)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\不可抢购页面.png)]
秒杀功能
session添加用户
在秒杀界面需要用户id,在session中放入userData值。
@Resource
UserAuthService userAuthService;
@RequestMapping(value = "/details", method = RequestMethod.GET)
public String Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession) {
//直接获取session中的UserData,有没有的逻辑交给service判断
UserData userData = (UserData) userAuthService.findUser(httpSession);
Goods goods = goodsService.selectGoodsDetails(goodsId);
通过service进行session中userData的判断与放置
@Resource
SelectUserByName selectUserByName;
public UserData findUser(HttpSession httpSession) {
//直接寻找
UserData userData = (UserData) httpSession.getAttribute("UserData");
//没有就去数据库查找用户,并放置session
if (userData==null){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
userData = selectUserByName.SelectUserByName(authentication.getName());
httpSession.setAttribute("UserData", userData);
}
return userData;
}
spring session实现
顺便实现sping sesssion,让关闭服务器后,同一session依然不需要登录。
redis安装:
redis GitHub Windows版本维护地址:https://github.com/tporadowski/redis/releases
GitHub 访问:使用https://ipaddress.com/website/github.global.ssl.fastly.net查询github ip,然后写入hosts文件并刷新dns。
第一次启动闪退,可以在安装位置 使用cmd依次执行:
redis-cli.exe
shutdown
exit
redis-server.exe redis.windows.conf
spring session实现:
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
<!--spring session依赖-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置redis:
#redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=10000000
打开redis-cli.exe允许rdb存储 config set stop-writes-on-bgsave-error no
UserData存入redis需要进行序列化
@Data
public class UserData implements Serializable {
int id;
String name;
String password;
String roles;
}
session存入redis
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ooPUVcCa-1664517455152)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\session存入redis.png)]
设置完毕后,只要打开redis-server,就可以实现持久化session,关闭服务器并再次打开,依然不需要登录。
秒杀过程
需要满足条件:
- 有库存才能秒杀
- 每个用户限购一件
订单数据库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2EvRUAU-1664517455153)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\订单数据库.png)]
详情页面传入商品id信息
<td>
<form th:action="@{/doSeckill(id=${goods.getId()})}" method="post">
<!--crsf的存在,必须在提交的form中写入以下信息-->
<input th:name="${_csrf.parameterName}" th:value="${_csrf.token}" type="hidden">
<!--抢购时需要传回商品ID-->
<input type="hidden" name="goodsId" th:value="${goods.getId()}">
<!--必须设置为btn才能禁用时变灰色 按钮为蓝色,充满一个区块,通过id控制是否可用-->
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即抢购</button>
</form>
</td>
购买逻辑实现
@RequestMapping(value = "/doSeckill",method = RequestMethod.POST)
public String DoSeckill(Model model, HttpSession httpSession,@RequestParam("id") int goodsId){
//取货物状态,判断库存
Goods goods = goodsService.selectGoodsDetails(goodsId);
if(goods.getStock()<1){
model.addAttribute("FailMessage","库存不足");
return "secKilFail";
}
//查看订单表,确认抢购一件
UserData userData = userAuthService.findUser(httpSession);
SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
if(secKillOrder!=null){
model.addAttribute("FailMessage","限购一件");
return "secKillFail";
}
//减去库存,产生订单
int nowStock = goods.getStock();
goods.setStock(nowStock);
goodsService.updateGoodsById(goods.getId(), goods.getStock());
goodsService.insertSecKillOrder(userData.getId(), goods.getId());
secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
model.addAttribute("SecKillOrder",secKillOrder);
model.addAttribute("goods", goods);
model.addAttribute("userData", userData);
return "secKillOrder";
}
订单页面
<thead>
<tr>
<th>id</th>
<th>商品</th>
<th>用户</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<td th:text="${SecKillOrder.getId()}">null</td>
<td th:text="${goods.getGoods_name()}">null</td>
<td th:text="${userData.getName()}">null</td>
<td>
<button class="btn btn-primary btn-block" type="submit" id="buyButton">立即支付</button>
</td>
</tbody>
订单展示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kogvQCcm-1664517455153)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\订单页面.png)]
jmeter使用
下载安装
进去jmeter官网下载binarie下的.zip文件,解压后在bin文件下打开jmter.bat
添加线程数,线程数*循环次数为总共的请求次数,设置在ramp-up内启动
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q4pnlwA7-1664517455154)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter线程数.png)]
添加请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rj9fGd9E-1664517455155)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加请求.png)]
已经登录后,添加cookie,设置session进行登录才能访问商品列表。
多个用户进行请求就添加多个cookie。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pIyD3YPk-1664517455156)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加cookie.png)]
添加监听器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YHN2MCPZ-1664517455157)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter添加监听器.png)]
查看结果树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yp905Asm-1664517455157)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter查看结果树.png)]
查看聚合报告,吞吐量(qps)为每秒中成功处理的请求次数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pi0M0TFl-1664517455158)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter查看聚合报告.png)]
所有接口测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TVEmGxda-1664517455159)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\jmeter所有接口测试.png)]
页面优化
优化前 400线程 qps 192
页面缓存
不仅是跳转页面,而是返回一个页面并放到redis中缓存。
查询reidis-》返回/手动渲染-》存入redis-》返回模板
更改前的商品列表controller
//详情页面
@RequestMapping("/index")
public String index(Model model){
List<Goods> goodsList = goodsService.selectGoodsList();
model.addAttribute("goodsList", goodsList);
return "index";
}
加入页面缓存的商品列表controller
@Resource
RedisTemplate redisTemplate;
@Resource
ThymeleafViewResolver thymeleafViewResolver;
redis存入的缓存页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TdZjRbuq-1664517455159)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\redis存入的缓存页面.png)]
url缓存
进入不同的详情页面,点击后具有不同的id,按照以下方式获取
String html = (String) valueOperations.get("detailsTemplate:"+goodsId);//需要具有不同的id
改变后的详情页面controller
//秒杀页面
@RequestMapping(value = "/details", method = RequestMethod.GET, produces = "text/html;charset=utf-8")
@ResponseBody
public String Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession, HttpServletRequest request, HttpServletResponse response) {
//进入不同的详情页面,缓存时需要存入id
ValueOperations valueOperations = redisTemplate.opsForValue();//拿一个redis中存储键值对的工具
String html = (String) valueOperations.get("detailsTemplate:"+goodsId);//需要具有不同的id
if(!StringUtils.isEmpty(html)){
return html;
}
//直接获取session中的UserData,有没有的逻辑交给service判断
UserData userData = userAuthService.findUser(httpSession);
Goods goods = goodsService.selectGoodsDetails(goodsId);
//状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
int secKillStatus = 0;
Date nowDate = new Date();
//秒杀未开始
if (goods.getBegin_time().after(nowDate)) {
secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
} else if (goods.getEnd_time().before(nowDate)) {
secKillStatus=-1;
}
model.addAttribute("goods", goods);
model.addAttribute("secKillStatus", secKillStatus);
//页面手动渲染
WebContext context = new WebContext(request, response, request.getServletContext(),
request.getLocale(),model.asMap());//模板渲染需要传入Map形式数据
html = thymeleafViewResolver.getTemplateEngine().process("details", context);//需要输入被渲染模板的名称
if(!StringUtils.isEmpty(html)){
//存入redis缓存
valueOperations.set("detailsTemplate:"+goodsId, html, 60, TimeUnit.SECONDS);//设置失效时间为1分钟
return html;
}
return null;
}
对象缓存
例如存入sping session到redis
需要注意的是更新密码后,需要清空redis中内容,保证数据一致性
加入页面缓存后
qps:192提高到qps:366
页面静态化
之前是前端需要渲染出整个带有服务端数据的页面,改为先渲染html,需要的数据再去查。
拆分出不需要变动的部分放到浏览器作缓存,只传输需要变动的部分(前端进行页面的跳转,ajax请求数据,后端只返回一个对象)
后端返回一个对象
值对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DetailsVo {
UserData userData;
Goods goods;
int secKillStatus;
}
商品详情页controller,返回一个对象
//静态化秒杀页面
@RequestMapping(value = "/details", method = RequestMethod.GET)
@ResponseBody
public DetailsVo Details(Model model, @RequestParam("id") int goodsId, HttpSession httpSession) {
//直接获取session中的UserData,有没有的逻辑交给service判断
UserData userData = userAuthService.findUser(httpSession);
Goods goods = goodsService.selectGoodsDetails(goodsId);
//状态:1(秒杀未开始),0(进行中),-1(已结束)默认为0
int secKillStatus = 0;
Date nowDate = new Date();
//秒杀未开始
if (goods.getBegin_time().after(nowDate)) {
secKillStatus=((int)(goods.getBegin_time().getTime()-nowDate.getTime())/1000);//秒杀为开始就将秒杀状态变为时间
} else if (goods.getEnd_time().before(nowDate)) {
secKillStatus=-1;
}
DetailsVo detailsVo = new DetailsVo();
detailsVo.setGoods(goods);
detailsVo.setSecKillStatus(secKillStatus);
detailsVo.setUserData(userData);
return detailsVo;
}
页面处理
防止thymeleaf默认渲染,将详情页面拷贝到static下作为静态页面,并更改后缀为htm,并更改跳转到详情的href
<a th:href="'/static/details.htm?id='+${goods.getId()}">详情</a>
去掉所有src的th:,整个html页面现在是静态资源
使用th:传值的地方改为id,用ajax进行传值
//获取数据代码
function getDetails(){
//获取url中的id参数,确定请求的url
var goodsId = g_getQueryString("id");
//ajax向后端请求数据
$.ajax({
url:'/details',//这里为请求controller中对应方法,一定为单引号
type:'GET',
data:{
"id":goodsId
},//请求传入的参数,后端可以用@RequestParam获取
// dataType:"json",
success:function(data){//成功返回data后,就进行值提取
render(data);
},
error:function (){
window.alert("function请求出错");
}
})
}
render将获得的值赋予界面
function render(detail) {
var goods = detail.goods;
var secKillStatus = detail.secKillStatus;
console.info("secKillStatus:"+secKillStatus);
$("#id").text(goods.id);
$("#goodsName").text(goods.goods_name);
$("#goodsPrice").text(goods.price);
$("#goodsStock").text(goods.stock);
/*$("#goodsBeginTime").text(new Date(goods.begin_time).format(goods.begin_time, 'yyyy-MM-dd hh:mm:ss'));
$("#goodsBeginTime").text(new Date(goods.end_time).format(goods.begin_time, 'yyyy-MM-dd hh:mm:ss'));*/
$("#goodsBeginTime").text(goods.begin_time);
$("#goodsEndTime").text(goods.end_time);
$("#secKillStatus").val(secKillStatus);
$("#goodsId").val(goods.id);
countDown();
}
秒杀操作静态化
直接在页面进行跳转,更改秒杀按钮
<button class="btn btn-primary btn-block" type="button" id="buyButton" onclick="doSeckill()">立即抢购</button>
controller判断是否减去库存,返回跳转信息
@RequestMapping(value = "/doSecKill",method = RequestMethod.GET)
@ResponseBody
public SecKillVo DoSeckill(Model model, HttpSession httpSession, @RequestParam("id") int goodsId){
//取货物状态,判断库存
Goods goods = goodsService.selectGoodsDetails(goodsId);
SecKillVo secKillVo =new SecKillVo();
if(goods.getStock()<1){
model.addAttribute("FailMessage","库存不足");
secKillVo.setMessage("Fail");
return secKillVo;
}
//查看订单表,确认抢购一件
UserData userData = userAuthService.findUser(httpSession);
SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
if(secKillOrder!=null){
model.addAttribute("FailMessage","限购一件");
secKillVo.setMessage("Fail");
return secKillVo;
}
//减去库存,产生订单
int nowStock = goods.getStock();
goods.setStock(nowStock-1);
goodsService.updateGoodsById(goods.getId(), goods.getStock());
goodsService.insertSecKillOrder(userData.getId(), goods.getId());
secKillVo.setMessage("Success");
return secKillVo;
}
js书写秒杀方法,向后台请求数据,并使用window.location.href跳转至订单页面
function doSecKill(){
var goodsId = $("#goodsId").val();
$.ajax(
{
url:'/doSecKill' ,
type: 'GET',
data:{
"id":goodsId//键要有双引号
},
success:function (data){
if(data.message=="Success"){
window.location.href="/static/secKillOrder.htm?id="+goodsId;
}
else {
window.location.href="/static/secKillFail.htm";
}
},
error:function (){
window.alert("function错误")
}
}
)
}
订单页面静态化
controller返回订单结果
@RequestMapping(value = "/OrderDetail",method = RequestMethod.GET)
@ResponseBody
public OrderDetailVo OrderDetail(Model model, HttpSession httpSession, @RequestParam("id") int goodsId){
Goods goods = goodsService.selectGoodsDetails(goodsId);
UserData userData = userAuthService.findUser(httpSession);
SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goods.getId());
OrderDetailVo orderDetail = new OrderDetailVo();
orderDetail.setSecKillOrder(secKillOrder);
orderDetail.setGoods(goods);
orderDetail.setUserData(userData);
return orderDetail;
}
订单页面ajax请求
$(function (){
OrderDetail();
});
function OrderDetail(){
var goodsId = g_getQueryString("id");
$.ajax({
url:'/OrderDetail',
type:'GET',
data:{
"id":goodsId
},
success:function (data){
var secKillOrder = data.secKillOrder;
var goods = data.goods;
var userData = data.userData;
$("#orderId").text(secKillOrder.id);
$("#goodsName").text(goods.goods_name);
$("#userName").text(userData.name);
},
error:function (){
window.alert("function错误")
}
})
}
//获取Url中参数
function g_getQueryString(name){
var reg = new RegExp("(^|&)"+name+"=([^&]*)(&|$)");//正则表达式
var r = window.location.search.substr(1).match(reg);//从?开始的url部分
if(r!=null) return unescape(r[2]);
return null;
}
超卖
库存一直减为负数:减去时判断库存是否大于0
两个请求同时发出,一个用户可能抢到两件商品:同一用户超卖,可以将uid和goodid组合添加为唯一索引
qps->290
服务优化
解决高并发
预减库存
减少数据库访问
在最开始将数据库中库存,加载到redis,每次从redis中减去库存,成功就给到消息队列
redis预减库存,不足直接返回。实现InitializingBean,在controller初始化时将库存加载到redis。
@Controller
public class MainController implements InitializingBean
@Override
public void afterPropertiesSet() throws Exception {
List<Goods> goodsList = goodsService.selectGoodsList();
if(goodsList==null){return;}
else {
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set("secKillGoods:"+goods.getId(), goods.getStock());
});
}
}
秒杀时的预减去库存
int stock = (int) redisTemplate.opsForValue().get("secKillGoods:"+goods.getId());
if(stock<=0){
model.addAttribute("FailMessage","库存不足");
secKillVo.setMessage("Fail");
return secKillVo;
}
内存标记
减少reids访问
定义存储标记的map
Map<Integer, Boolean> EmptyStock =new HashMap<>();
初始化时,将标记设置为false
@Override
public void afterPropertiesSet() throws Exception {
List<Goods> goodsList = goodsService.selectGoodsList();
if(goodsList==null){return;}
else {
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set("secKillGoods:"+goods.getId(), goods.getStock());
EmptyStock.put(goods.getId(), false);
});
}
}
当库存为0,设置内存标记为true
if(stock<=0){
model.addAttribute("FailMessage","库存不足");
secKillVo.setMessage("Fail");
EmptyStock.put(goodsId,true);
return secKillVo;
}
接下来再进行尝试减去库存之前,如果内存标记为true,直接返回失败
if (EmptyStock.get(goodsId)){
secKillVo.setMessage("Fail");
return secKillVo;
}
int stock = (int) redisTemplate.opsForValue().get("secKillGoods:"+goods.getId());
队列消息缓冲
ubuntu 22.04中安装
sudo apt install erlang
sudo apt install rabbitmq-server
查询安装状态、开启管理面板
sudo rabbitmqctl status
sudo rabbitmq-plugins enable rabbitmq_management
linux中使用ifconfig -a查看本机网络ip 本地网络ip:15672 可访问控制面板
guest用户只能本地登录 因此创建新的用户
sudo rabbitmqctl add_user 用户名 密码
sudo rabbitmqctl set_user_tags 用户名 administrator
交换机(使用@Bean方法中return new不同类型的交换机):fanout(广播)、direct(使用routing key交换机和队列)、topic(仍然使用routing 不过是通配符 *匹配一个词语 #匹配一个或多个词语)
SpringBoot集成RabbitMQ:
1.添加RabbitMQ依赖(amqp)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置RabbitMQ(主机(host)、用户名(username),密码(password))192.168.209.129
#rabbimq
spring.rabbitmq.host=192.168.209.129
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
3.config中RabbitMQ中配置类,这里使用topic模式(方法上@Bean, return new Queue(“队列名称”, 是否持久化), return new TopicExchange(“交换机名称”, 是否持久化), return new Bindingbuilder.bind().with(“匹配的队列名称”))
@Configuration
public class RabbitMQConfiguration {
//队列
@Bean
public Queue queue(){
return new Queue("seckillQueue");
}
//交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("seckillExchange");
}
//绑定
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with("secKill.#");
}
}
4.消息定义(实体类(秒杀信息),返回用户和货物)
public class SeckillMessage {
Goods goods;
UserData userData;
}
5.消息发送者(rabbimq包下,类上@Service ,注入RabbimqTemplate,定义send方法,使用模板中convertAndSend.(“交换机名称”,“路由名称”,消息)
导入依赖
<!--json与对象互转-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
@Service
public class RabbiMQSender {
@Resource
RabbitTemplate rabbitTemplate;
public void send(SeckillMessage seckillMessage){
rabbitTemplate.convertAndSend("seckillExchange", "seckillMessage",JSON.toJSONString(seckillMessage));
}
}
6.消息接收者(rabbimq包下,类上@Service , @RabbitMQ(“队列名称”), 监听队列),获取信息后,进行下单操作
@Service
public class RabbitMQRecive {
@Resource
GoodsService goodsService;
@RabbitListener(queues = "seckillQueue")
public SecKillVo receive(String message){
SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);
//进行秒杀
Goods goods = seckillMessage.getGoods();
UserData userData = seckillMessage.getUserData();
int nowStock = goods.getStock();
goods.setStock(nowStock-1);
goodsService.updateGoodsById(goods.getId(), goods.getStock());
goodsService.insertSecKillOrder(userData.getId(), goods.getId());
SecKillVo secKillVo = new SecKillVo();
secKillVo.setMessage("Wait");
return secKillVo;
}
}
7.controller调用发送消息并返回等待
SeckillMessage seckillMessage = new SeckillMessage(goods, userData);
rabbiMQSender.send(seckillMessage);
secKillVo.setMessage("Wait");
return secKillVo;
8.客户端轮询(去在客户端中查询订单表,如果获取就返回1表示有订单)
function doSecKill(){
var goodsId = $("#goodsId").val();
$.ajax(
{
url:'/doSecKill' ,
type: 'GET',
data:{
"id":goodsId//键要有双引号
},
success:function (data){
if(data.message=="Wait"){
getResult(id);
}
else {
window.location.href="/static/secKillFail.htm";
}
},
error:function (){
window.alert("function错误")
}
}
)
}
function getResult(goodsId){
$.ajax({
ulr:'/getResult',
type:'GET',
data:{
"id":goodsId//键要有双引号
},
success:function (data){
if(data.message=="Wait"){
setTimeout(function (){
getResult(goodsId);
},50)}
else {
layer.confirm("秒杀成功,查看订单?", {btn:["确认","取消"]},
function (){
window.location.href="/static/secKillOrder.htm"+goodsId;
})
}
}
})
}
9.查询查询订单的controller
@RequestMapping(value = "/getResult",method = RequestMethod.GET)
public SecKillVo getResult(HttpSession httpSession, @RequestParam("id") int goodsId){
UserData userData = userAuthService.findUser(httpSession);
SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goodsId);
SecKillVo secKillVo = new SecKillVo();
secKillVo.setMessage("Success");
return secKillVo;
}
配置类中定义消息队列,sercie中定义与队列绑定的生产者消费者,contrller中发送一个消息
定义队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lahYRk2C-1664517455161)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列配置.png)]
controller传入用户和商品id,使用消息队列处理,当给到消息队列后,返回0表示排队中。
最终的下单结果,在客户端中的js使用轮询查看是否真正下单成功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOY5S8iM-1664517455162)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\controller使用消息队列.png)]
发送消息到队列,请求生成订单,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9J7t8fR1-1664517455162)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列发送消息.png)]
从队列中取得消息,尝试生成订单
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59J5Fi8r-1664517455163)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\消息队列消费消息.png)]
客户端轮询结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFNsdmDU-1664517455164)(C:\Users\DeLL\Desktop\文档\就业资料\java面试资料\java总结\客户端轮询结果.png)]
reids分布式锁
原本的redis递增递减带有原子性,可以优使用redis分布式锁优化。
安全优化
秒杀接口隐藏
每个用户拥有不同的秒杀接口地址,防止脚本不断的刷新。现在脚本无法得到真正的秒杀接口,因为秒杀接口需要特定生成。防止攻击地址,现在需要点击秒杀按钮才能得到真正的地址,并且每个用户不一样。这样同一个用户多次点击攻击后,可以使用接口限流隐藏掉。
每次点击秒杀,不是直接秒杀,而是获取接口地址,同时中间加验证码,防止脚本。
点击秒杀按钮后,首先均访问/path的Controller,在该Controller中生成唯一的接口地址,存储在redis中key为skillpath+uerid+goodsid,值为生成的秒杀地址,并在前端页面接受。前端接受到生成的path之后,进行真正的秒杀。
接口限流
每个用户请求几次后,禁止请求。
用户判断:ConcurrentHashmap(用户,次数)
goodsId;
})
}
}
})
}
9.查询查询订单的controller
```java
@RequestMapping(value = "/getResult",method = RequestMethod.GET)
public SecKillVo getResult(HttpSession httpSession, @RequestParam("id") int goodsId){
UserData userData = userAuthService.findUser(httpSession);
SecKillOrder secKillOrder = goodsService.selectOrder(userData.getId(), goodsId);
SecKillVo secKillVo = new SecKillVo();
secKillVo.setMessage("Success");
return secKillVo;
}
配置类中定义消息队列,sercie中定义与队列绑定的生产者消费者,contrller中发送一个消息
定义队列
[外链图片转存中…(img-lahYRk2C-1664517455161)]
controller传入用户和商品id,使用消息队列处理,当给到消息队列后,返回0表示排队中。
最终的下单结果,在客户端中的js使用轮询查看是否真正下单成功。
[外链图片转存中…(img-XOY5S8iM-1664517455162)]
发送消息到队列,请求生成订单,
[外链图片转存中…(img-9J7t8fR1-1664517455162)]
从队列中取得消息,尝试生成订单
[外链图片转存中…(img-59J5Fi8r-1664517455163)]
客户端轮询结果
[外链图片转存中…(img-PFNsdmDU-1664517455164)]
reids分布式锁
原本的redis递增递减带有原子性,可以优使用redis分布式锁优化。
安全优化
秒杀接口隐藏
每个用户拥有不同的秒杀接口地址,防止脚本不断的刷新。现在脚本无法得到真正的秒杀接口,因为秒杀接口需要特定生成。防止攻击地址,现在需要点击秒杀按钮才能得到真正的地址,并且每个用户不一样。这样同一个用户多次点击攻击后,可以使用接口限流隐藏掉。
每次点击秒杀,不是直接秒杀,而是获取接口地址,同时中间加验证码,防止脚本。
点击秒杀按钮后,首先均访问/path的Controller,在该Controller中生成唯一的接口地址,存储在redis中key为skillpath+uerid+goodsid,值为生成的秒杀地址,并在前端页面接受。前端接受到生成的path之后,进行真正的秒杀。
接口限流
每个用户请求几次后,禁止请求。
用户判断:ConcurrentHashmap(用户,次数)