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加锁即可