1.Redis预减库存
1.OrderServiceImpl.java 问题分析
2.具体实现 SeckillController.java
1.实现InitializingBean接口的afterPropertiesSet方法,在bean初始化之后将库存信息加载到Redis
/**
* 系统初始化,将秒杀商品库存加载到redis中
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// 将秒杀商品库存加载到redis中
List<GoodsVo> goodsVoList = goodsService.findGoodsVo();
// 如果没有秒杀商品,直接返回
if (CollectionUtils.isEmpty(goodsVoList)) {
return;
}
goodsVoList.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount());
});
}
2.进行库存预减
// 库存预减
Long stock = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
// 判断库存是否充足
if (stock < 0) {
// 库存不足,返回秒杀失败页面
redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
return "secKillFail";
}
3.优化分析
- 正常情况下,每次都需要到数据库减少库存,来解决超卖问题
- 使用Redis进行库存预减,可以减少对数据库的操作,从而提升效率
4.测试
1.清空Redis
2.清空订单表和秒杀商品表,设置一号商品库存为10
3.将项目部署上线
4.UserUtil.java生成100个用户
5.发送5000次请求
1.线程组配置
2.cookie管理器
3.秒杀请求
4.QPS为307,从80提升到了307提升了283%
5.但是,出现了库存遗留问题
5.缓存遗留原因分析
2.内存标记优化高并发
1.问题分析
- 在未使用内存标记时,每次请求都需要对库存进行预减,来判断是否有库存,即使库存为0
- 所以采用内存标记的方式,当库存为0的时候,就不用进行库存预减
2.具体实现 SeckillController.java
1.首先定义一个标记是否有库存的map
2.在系统初始化时,初始化map
3.如果库存预减发现没有库存了,就设置内存标记
4.在库存预减前,判断内存标记,减少redis访问
3.测试
1.将项目上线
2.清空订单表和秒杀商品表,设置一号商品库存为10
3.清空Redis
4.发送5000次请求,QPS为330,从307提高到了330
3.消息队列实现异步秒杀
1.问题分析
2.思路分析
3.构建秒杀消息对象 SeckillMessage.java
package com.sxs.seckill.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Description: 秒杀消息
*
* @Author sun
* @Create 2024/5/13 14:15
* @Version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
private User user;
private Long goodsId;
}
4.秒杀RabbitMQ配置
package com.sxs.seckill.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Description: 秒杀RabbitMQ配置
*
* @Author sun
* @Create 2024/5/13 14:23
* @Version 1.0
*/
@Configuration
public class RabbitMQSeckillConfig {
// 定义一个消息队列和一个topic交换机的名字
public static final String SECKILL_QUEUE = "seckillQueue";
public static final String SECKILL_EXCHANGE = "seckillExchange";
// 创建一个消息队列
@Bean
public Queue seckillQueue() {
return new Queue(SECKILL_QUEUE, true);
}
// 创建一个topic交换机
@Bean
public TopicExchange seckillExchange() {
return new TopicExchange(SECKILL_EXCHANGE);
}
// 将消息队列绑定到交换机
@Bean
public Binding binding() {
// 绑定消息队列到交换机,并指定routingKey,表示只接收routingKey为seckill.#的消息
return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with("seckill.#");
}
}
5.生产者和消费者
1.生产者 MQSendMessage.java
package com.sxs.seckill.rabbitmq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* Description: 消息队列发送消息
*
* @Author sun
* @Create 2024/5/13 15:14
* @Version 1.0
*/
@Service
@Slf4j
public class MQSendMessage {
@Resource
private RabbitTemplate rabbitTemplate;
// 发送秒杀消息
public void sendSeckillMessage(String message) {
log.info("发送消息:" + message);
rabbitTemplate.convertAndSend("seckillExchange", "seckill.message", message);
}
}
2.消费者,进行秒杀
1.引入hutool工具类
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
2. MQReceiverMessage.java
package com.sxs.seckill.rabbitmq;
import cn.hutool.json.JSONUtil;
import com.sxs.seckill.pojo.SeckillMessage;
import com.sxs.seckill.pojo.User;
import com.sxs.seckill.service.GoodsService;
import com.sxs.seckill.service.OrderService;
import com.sxs.seckill.service.SeckillGoodsService;
import com.sxs.seckill.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* Description: 消息队列接收消息
*
* @Author sun
* @Create 2024/5/13 15:17
* @Version 1.0
*/
@Service
@Slf4j
public class MQReceiverMessage {
@Resource
private GoodsService goodsService;
@Resource
private OrderService orderService;
// 接收秒杀消息
@RabbitListener(queues = "seckillQueue")
public void receiveSeckillMessage(String message) {
log.info("接收消息:" + message);
// 此时的message是秒杀的消息,要将其转换为SeckillMessage对象
SeckillMessage seckillMessage = JSONUtil.toBean(message, SeckillMessage.class);
// 获取秒杀信息
User user = seckillMessage.getUser();
Long goodsId = seckillMessage.getGoodsId();
// 根据商品id查询商品详情
GoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId);
// 进行秒杀
orderService.seckill(user, goodsVoByGoodsId);
}
}
6.编写控制层
1.SeckillController.java
// MQ实现异步秒杀
// 封装秒杀信息
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送
mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));
// 返回排队中页面
model.addAttribute("errmsg", RespBeanEnum.QUEUE_ERROR.getMessage());
return "secKillFail";
2.RespBeanEnum.java 新增响应枚举类
7.测试
1.将项目上线
2.清空订单表和秒杀商品表,设置一号商品库存为10
3.清空Redis
4.发送5000次请求,QPS为363
秒杀安全
1.秒杀接口隐藏
1.需求分析
2.思路分析
3.具体实现
1.RespBeanEnum.java 新增几个响应
2.OrderService.java 新增方法
/**
* 方法:生成秒杀路径
* @param user
* @param goodsId
* @return
*/
String createPath(User user, Long goodsId);
/**
* 方法:校验秒杀路径
* @param user
* @param goodsId
* @param path
* @return
*/
boolean checkPath(User user, Long goodsId, String path);
3.OrderServiceImpl.java
@Override
public String createPath(User user, Long goodsId) {
// 对参数进行校验
if (user == null || goodsId <= 0) {
return null;
}
// 生成秒杀路径
String path = MD5Util.md5(UUIDUtil.uuid() + "123456");
// 保存到redis中,设置过期时间为60秒
redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" + goodsId, path, 60, TimeUnit.SECONDS);
return path;
}
@Override
public boolean checkPath(User user, Long goodsId, String path) {
// 对参数进行校验
if (user == null || goodsId <= 0 || StringUtils.isBlank(path)) {
return false;
}
// 从redis中获取秒杀路径
String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
// 判断是否相等,并返回
return path.equals(redisPath);
}
4.SeckillController.java
@RequestMapping("/{path}/doSeckill")
public RespBean doSeckill(Model model, User user, Long goodsId, @PathVariable String path) {
// 判断用户是否登录
if (user == null) {
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
// 校验path
boolean check = orderService.checkPath(user, goodsId, path);
if (!check) {
return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
}
// 根据goodsId获取GoodsVo
GoodsVo goodsVoByGoodsId = goodsService.findGoodsVoByGoodsId(goodsId);
// 判断是否有库存
if (goodsVoByGoodsId.getStockCount() < 1) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
// 从redis中判断是否复购
if (redisTemplate.hasKey("order:" + user.getId() + ":" + goodsId)) {
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
// 首先判断内存标记
if (inventoryTagging.get(goodsId)) {
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
// 库存预减
Long stock = redisTemplate.opsForValue().decrement("seckillGoods:" + goodsId);
// 判断库存是否充足
if (stock < 0) {
// 标记库存不足
inventoryTagging.put(goodsId, true);
// 库存不足,返回秒杀失败页面
redisTemplate.opsForValue().increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
// MQ实现异步秒杀
// 封装秒杀信息
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
// 使用hutool工具类将SeckillMessage对象转换为json字符串并发送
mqSendMessage.sendSeckillMessage(JSONUtil.toJsonStr(seckillMessage));
// 返回排队中
return RespBean.success(RespBeanEnum.SEK_KILL_WAIT);
}
/**
* 生成秒杀地址
* @param user
* @param goodsId
* @return
*/
@ResponseBody
@RequestMapping("/path")
public RespBean getPath(User user, Long goodsId) {
// 参数校验
if (user == null || goodsId <= 0) {
return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
}
// 调用OrderService中的createPath方法生成秒杀地址
String path = orderService.createPath(user, goodsId);
return RespBean.success(path);
}
5.goodsDetail.html
1.秒杀首先获取路径
2.解析环境变量,区分多环境
3.新增两个方法,使用隐藏秒杀接口的方式秒杀商品
4.测试
2.验证码防止脚本攻击
1.思路分析
2.具体实现
1.pom.xml 引入依赖
<dependency>
<groupId>com.ramostear</groupId>
<artifactId>Happy-Captcha</artifactId>
<version>1.0.1</version>
</dependency>
2.SeckillController.java 编写方法生成验证码
/**
* 生成验证码
* @param user
* @param goodsId
* @param request
* @param response
*/
@RequestMapping("/captcha")
public void happyCaptcha(User user, Long goodsId, HttpServletRequest request, HttpServletResponse response) {
HappyCaptcha.require(request, response)
.style(CaptchaStyle.ANIM) //设置展现样式为动画
.type(CaptchaType.NUMBER) //设置验证码内容为数字
.length(6) //设置字符长度为 6
.width(220) //设置动画宽度为 220
.height(80) //设置动画高度为 80
.font(Fonts.getInstance().zhFont()) //设置汉字的字体
.build().finish(); //生成并输出验证码
// 这个验证码的结果会存储在session中,可以通过request.getSession().getAttribute("happy-captcha")获取
// 获取验证码的值,放入redis中
String verifyCode = request.getSession().getAttribute("happy-captcha").toString();
redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, verifyCode, 60, TimeUnit.SECONDS);
}
3.OrderService.java 校验用户输入的验证码
/**
* 校验用户输入的验证码
* @param user
* @param goodsId
* @param captcha
* @return
*/
boolean checkCaptcha(User user, Long goodsId, String captcha);
4.OrderServiceImpl.java
@Override
public boolean checkCaptcha(User user, Long goodsId, String captcha) {
// 参数校验
if (user == null || goodsId <= 0 || StringUtils.isBlank(captcha)) {
return false;
}
// 从redis中获取验证码
String verifyCode = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
return captcha.equals(verifyCode);
}
5.SeckillController.java 加入验证码校验
6.goodsDetail.html
1.前端请求验证码
2.测试
3.获取用户输入的验证码,并携带验证码
3.秒杀接口限流-防刷
1.思路分析
2.简单接口限流
1.SeckillController.java
2.测试
4.通用接口限流防刷
1.思路分析
2.编写自定义限流注解 AccessLimit.java
package com.sxs.seckill.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Description: 限流注解
*
* @Author sun
* @Create 2024/5/14 15:38
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Target(ElementType.METHOD) // 作用在方法上
public @interface AccessLimit {
int seconds(); // 时间范围
int maxCount(); // 最大访问次数
boolean needLogin() default true; // 是否需要登录
}
3.使用方式 SeckillController.java
4.编写 config/UserContext.java 使用ThreadLocal存储user
package com.sxs.seckill.config;
import com.sxs.seckill.pojo.User;
/**
* Description:
*
* @Author sun
* @Create 2024/5/14 15:46
* @Version 1.0
*/
public class UserContext {
// 初始化ThreadLocal以存储用户信息
private static ThreadLocal<User> threadLocal = new ThreadLocal<>();
public static User getUser() {
return threadLocal.get();
}
public static void setUser(User user) {
threadLocal.set(user);
}
// 清除ThreadLocal中的数据
public static void removeUser() {
threadLocal.remove();
}
}
5.编写自定义限流拦截器 config/AccessLimitInterceptor.java
package com.sxs.seckill.config;
import com.sxs.seckill.exception.GlobalException;
import com.sxs.seckill.pojo.User;
import com.sxs.seckill.service.UserService;
import com.sxs.seckill.utils.CookieUtil;
import com.sxs.seckill.vo.RespBeanEnum;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* Description: 限流拦截器
*
* @Author sun
* @Create 2024/5/14 15:55
* @Version 1.0
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Resource
RedisTemplate redisTemplate;
/**
* 拦截请求,进行限流处理,在目标方法前执行
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
// 如果是方法级别的拦截
// 1.获取user对象,放到threadLocal中
User user = getUser(request, response);
UserContext.setUser(user);
// 2.处理限流注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
// 3.获取注解上的参数
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if (needLogin) {
// 如果需要登录,但是没有登录,返回错误信息
if (user == null) {
// 如果需要登录,但是没有登录,返回错误信息
throw new GlobalException(RespBeanEnum.USER_NOT_LOGIN);
}
// 如果登录了,key加上用户id
key += ":" + user.getId();
}
// 4.对访问次数进行限制,如果登陆了就是对这个用户的访问次数进行限制,如果没有登录就是对这个接口的访问次数进行限制
Integer count = (Integer) redisTemplate.opsForValue().get(key);
if (count == null) {
// 第一次访问
redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
} else if (count < maxCount) {
// 访问次数加1
redisTemplate.opsForValue().increment(key);
} else {
// 超过访问次数
throw new GlobalException(RespBeanEnum.ACCESS_LIMIT_REACHED);
}
}
// 如果不是方法级别的拦截,直接放行
return true;
}
// 单独编写方法,获取User对象
private User getUser(HttpServletRequest request, HttpServletResponse response) {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if (ticket == null) {
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
6.config/WebConfig.java中注册拦截器
7.修改自定义参数解析器UserArgumentResolver.java,直接从ThreadLocal中获取User
8.测试
9.解决库存遗留问题,为每个用户id加锁即可
最后
从时代发展的角度看,网络安全的知识是学不完的,而且以后要学的会更多,同学们要摆正心态,既然选择入门网络安全,就不能仅仅只是入门程度而已,能力越强机会才越多。
因为入门学习阶段知识点比较杂,所以我讲得比较笼统,大家如果有不懂的地方可以找我咨询,我保证知无不言言无不尽,需要相关资料也可以找我要,我的网盘里一大堆资料都在吃灰呢。
干货主要有:
①1000+CTF历届题库(主流和经典的应该都有了)
②CTF技术文档(最全中文版)
③项目源码(四五十个有趣且经典的练手项目及源码)
④ CTF大赛、web安全、渗透测试方面的视频(适合小白学习)
⑤ 网络安全学习路线图(告别不入流的学习)
⑥ CTF/渗透测试工具镜像文件大全
⑦ 2023密码学/隐身术/PWN技术手册大全
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
扫码领取