1.Redis预减库存
1.OrderServiceImpl.java 问题分析
2.具体实现 SeckillController.java
1.实现InitializingBean接口的afterPropertiesSet方法,在bean初始化之后将库存信息加载到Redis
@Override
public void afterPropertiesSet ( ) throws Exception {
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 ;
@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 ;
@Configuration
public class RabbitMQSeckillConfig {
public static final String SECKILL_QUEUE = "seckillQueue" ;
public static final String SECKILL_EXCHANGE = "seckillExchange" ;
@Bean
public Queue seckillQueue ( ) {
return new Queue ( SECKILL_QUEUE , true ) ;
}
@Bean
public TopicExchange seckillExchange ( ) {
return new TopicExchange ( SECKILL_EXCHANGE ) ;
}
@Bean
public Binding binding ( ) {
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 ;
@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 ;
@Service
@Slf4j
public class MQReceiverMessage {
@Resource
private GoodsService goodsService;
@Resource
private OrderService orderService;
@RabbitListener ( queues = "seckillQueue" )
public void receiveSeckillMessage ( String message) {
log. info ( "接收消息:" + message) ;
SeckillMessage seckillMessage = JSONUtil . toBean ( message, SeckillMessage . class ) ;
User user = seckillMessage. getUser ( ) ;
Long goodsId = seckillMessage. getGoodsId ( ) ;
GoodsVo goodsVoByGoodsId = goodsService. findGoodsVoByGoodsId ( goodsId) ;
orderService. seckill ( user, goodsVoByGoodsId) ;
}
}
6.编写控制层
1.SeckillController.java
SeckillMessage seckillMessage = new SeckillMessage ( user, goodsId) ;
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 新增方法
String createPath ( User user, Long goodsId) ;
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" ) ;
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 ;
}
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 ) ;
}
boolean check = orderService. checkPath ( user, goodsId, path) ;
if ( ! check) {
return RespBean . error ( RespBeanEnum . REQUEST_ILLEGAL ) ;
}
GoodsVo goodsVoByGoodsId = goodsService. findGoodsVoByGoodsId ( goodsId) ;
if ( goodsVoByGoodsId. getStockCount ( ) < 1 ) {
return RespBean . error ( RespBeanEnum . EMPTY_STOCK ) ;
}
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 ) ;
}
SeckillMessage seckillMessage = new SeckillMessage ( user, goodsId) ;
mqSendMessage. sendSeckillMessage ( JSONUtil . toJsonStr ( seckillMessage) ) ;
return RespBean . success ( RespBeanEnum . SEK_KILL_WAIT ) ;
}
@ResponseBody
@RequestMapping ( "/path" )
public RespBean getPath ( User user, Long goodsId) {
if ( user == null || goodsId <= 0 ) {
return RespBean . error ( RespBeanEnum . REQUEST_ILLEGAL ) ;
}
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 编写方法生成验证码
@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 )
. width ( 220 )
. height ( 80 )
. font ( Fonts . getInstance ( ) . zhFont ( ) )
. build ( ) . finish ( ) ;
String verifyCode = request. getSession ( ) . getAttribute ( "happy-captcha" ) . toString ( ) ;
redisTemplate. opsForValue ( ) . set ( "captcha:" + user. getId ( ) + ":" + goodsId, verifyCode, 60 , TimeUnit . SECONDS ) ;
}
3.OrderService.java 校验用户输入的验证码
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 ;
}
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 ;
@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 ;
public class UserContext {
private static ThreadLocal < User > threadLocal = new ThreadLocal < > ( ) ;
public static User getUser ( ) {
return threadLocal. get ( ) ;
}
public static void setUser ( User user) {
threadLocal. set ( user) ;
}
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 ;
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Resource
private UserService userService;
@Resource
RedisTemplate redisTemplate;
@Override
public boolean preHandle ( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ( handler instanceof HandlerMethod ) {
User user = getUser ( request, response) ;
UserContext . setUser ( user) ;
HandlerMethod handlerMethod = ( HandlerMethod ) handler;
AccessLimit accessLimit = handlerMethod. getMethodAnnotation ( AccessLimit . class ) ;
if ( accessLimit == null ) {
return true ;
}
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 += ":" + user. getId ( ) ;
}
Integer count = ( Integer ) redisTemplate. opsForValue ( ) . get ( key) ;
if ( count == null ) {
redisTemplate. opsForValue ( ) . set ( key, 1 , seconds, TimeUnit . SECONDS ) ;
} else if ( count < maxCount) {
redisTemplate. opsForValue ( ) . increment ( key) ;
} else {
throw new GlobalException ( RespBeanEnum . ACCESS_LIMIT_REACHED ) ;
}
}
return true ;
}
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加锁即可