导入依赖
<dependencies>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
</dependencies>
配置文件
spring:
thymeleaf配置
thymeleaf:
cache: false
数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: hsp
使用hikari数据源
hikari:
连接池名
pool-name: DateHikariCP
最小空闲连接数
minimum-idle: 5
空闲连接最大存活时间
idle-timeout: 180000
最大连接数
maximum-pool-size: 10
自动提交连接池返回的数据
auto-commit: true
max-lifetime: 180000
连接超时时间
connection-timeout: 30000
测试连接是否可用的查询语句
connection-test-query: SELECT 1
mybatis-plus:
mapper.xml映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
取别名的包
type-aliases-package: com.mgy.pojo
SQL打印(在方法接口所在的包)
logging:
level:
com.mgy.mapper: debug
创建包
- controller
- mapper
- pojo
- service
- resource中的mapper
- 主文件中加入注释:@MapperScan(“com.mgy.mapper”):代表该包下每个接口都得到实现类
测试controller
- DemoController
@Controller
@RequestMapping("/demo")
public class DemoController {
@RequestMapping("/hello")
public String hello(Model model){
model.addAttribute("name","mgy");
return "hello";
}
}
- 前端页面(在templates端口下)
sql表创建
CREATE TABLE t_user(
`id` BIGINT(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` VARCHAR(255) NOT NULL,
`password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
`slat` VARCHAR(10) DEFAULT NULL,
`head` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`register_date` DATETIME DEFAULT NULL COMMENT '注册时间',
`last_login_date` DATETIME DEFAULT NULL COMMENT '最后一次登录事件',
`login_count` INT(11) DEFAULT '0' COMMENT '登录次数',
PRIMARY KEY(`id`)
)
- 两次MD5加密,一次输入密码时加密(前端传给后端),存入数据库时再加密
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
@Component
public class MD5Util {
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
private static final String salt="1a2b3c4d";
public static String inputPassToFormPass(String inputPass){
String str = salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
public static String formPassToDBPass(String formPass,String salt){
String str = salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
return md5(str);
}
public static String inputPassToDBPass(String inputPass,String salt){
String formPass = inputPassToFormPass(inputPass);
String dbPass = formPassToDBPass(formPass,salt);
return dbPass;
}
}
创建逆向工程的模块
规定返回类型
登录页面
- 一个方法跳转登录页面
- 登录页面将信息提交到doLogin
RestController=Controller+ResponseBody
手机号码格式校验
@Pattern(regexp = "[1]([3-9])[0-9]{9}$",message = "手机号码格式错误")
- @Valid
- 异常捕获
- @RestControllerAdvice
- @ExceptionHandler(Exception.class)
登录信息传递
String ticket = UUIDUtil.uuid();
request.getSession().setAttribute(ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
public String toList(HttpSession session, Model model, @CookieValue("userTicket") String ticket){
if(StringUtils.isEmpty(ticket)){
return "login";
}
User user = (User)session.getAttribute(ticket);
if(user==null){
return "login";
}
model.addAttribute("user",user);
return "goodsList";
}
分布式session
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 使用redis保存用户登录的信息,就可以实现任何tomcat存放的数据都能被从redis中取出
public User getUserByCookie(String userTicket,HttpServletRequest request,HttpServletResponse response) {
if(StringUtils.isEmpty(userTicket)){
return null;
}
User user = (User)redisTemplate.opsForValue().get("user:"+userTicket);
if(user!=null){
CookieUtil.setCookie(request,response,"userTicket",userTicket);
}
return user;
}
String ticket = UUIDUtil.uuid();
redisTemplate.opsForValue().set("user:"+ticket,user);
CookieUtil.setCookie(request,response,"userTicket",ticket);
使用拦截器完善登录功能
- 用于这个老师教的方法没法没登录就都跳转到login界面,所以配置拦截器实现
@Component
public class AdminInterceptor implements HandlerInterceptor {
@Autowired
private IUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if(ticket==null){
response.sendRedirect(request.getContextPath()+"/login/toLogin");
}
User user = userService.getUserByCookie(ticket, request, response);
if(user==null){
response.sendRedirect(request.getContextPath()+"/login/toLogin");
}else{
return true;
}
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AdminInterceptor adminInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(adminInterceptor);
registration.addPathPatterns("/**").excludePathPatterns("/login/toLogin","/login.html","/login/doLogin");
}
}
秒杀详情页面准备
- 由于秒杀详情页面有来自goods表的信息,还有来自seckill_goods表的信息,因此用一个新的类GoodsVo将两者关联
解决静态资源无法访问问题
- 用webConfig进行配置之后,会覆盖原本的配置,导致静态资源无法访问,因此需要给静态资源放行
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AdminInterceptor adminInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(adminInterceptor);
registration.addPathPatterns("/**").excludePathPatterns("/login","/login.html","/login/toLogin","/login/doLogin","/static/**","/static/");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
- 注意,这里处理完之后要改一下前端页面,给前端页面的资源都加上/static/,才能成功被放行
秒杀功能的实现
- 首先进行判断,判断库存是否充足。判断是否已经抢购过
- 业务过程
- 让秒杀库存中的数量减少一个
- 新增一个订单
- 新增秒杀订单
- 外键的功能直接通过业务层实现,而不用做表之间的关联
- 通过自动返回主键的功能,得到order的Id,从而创建secKillOrder的Id
下面开始做优化功能
页面缓存
- 页面缓存就是将加载过的页面转成字符串存在redis里,直接取出来用
@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private IUserService userService;
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ThymeleafViewResolver thymeleafViewResolver;
@RequestMapping(value="/toList",produces = "text/html;charset=utf-8")
@ResponseBody
public String toList(Model model,HttpServletRequest request,HttpServletResponse response){
ValueOperations valueOperations = redisTemplate.opsForValue();
String html = (String)valueOperations.get("goodsList");
if(!StringUtils.isEmpty(html)){
return html;
}
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
model.addAttribute("user",user);
model.addAttribute("goodsList",goodsService.findGoodsVo());
WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodsList", webContext);
if(!StringUtils.isEmpty(html)){
valueOperations.set("goodsList",html,60, TimeUnit.MINUTES);
}
return html;
}
@RequestMapping(value="/toDetail/{goodsId}",produces = "text/html;charset=utf-8")
@ResponseBody
public String toDetail(@PathVariable Long goodsId,Model model,HttpServletRequest request,HttpServletResponse response){
ValueOperations valueOperations = redisTemplate.opsForValue();
String html = (String)valueOperations.get("goodsDetail"+goodsId);
if(!StringUtils.isEmpty(html)){
return html;
}
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
model.addAttribute("user",user);
GoodsVo goods = goodsService.findGoodsByGoodsId(goodsId);
model.addAttribute("goods",goods);
Date startDate = goods.getStartDate();
Date endDate = goods.getEndDate();
Date nowDate = new Date();
int secKillStatus = 0;
int remainSeconds = 0;
if(nowDate.before(startDate)){
remainSeconds = (int)((startDate.getTime()-nowDate.getTime())/1000);
}else if(nowDate.after(endDate)){
secKillStatus = 2;
}else{
secKillStatus = 1;
}
model.addAttribute("secKillStatus",secKillStatus);
model.addAttribute("remainSeconds",remainSeconds);
WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", webContext);
if(!StringUtils.isEmpty(html)){
valueOperations.set("goodsDetail"+goodsId,html,60, TimeUnit.MINUTES);
}
return html;
}
}
前后端分离
- 前后端分离能保证每次变化的数据尽可能少,将不会变的静态数据缓存在浏览器里,每次只需要服务器传输需要变化的数据
- 技术用的是之前学过的ajax,不做赘述
解决库存超卖问题
seckillGoods.setStockCount(goods.getStockCount()-1);
UpdateWrapper wrapper = new UpdateWrapper<SeckillGoods>().setSql("stock_count=stock_count-1")
.eq("id",seckillGoods.getId()).gt("stock_count",0);
boolean seckillResult = seckillGoodsService.update(wrapper);
if(!seckillResult){
return null;
}
- 在order表中创建一个用户id和商品id的唯一索引,防止一个用户同一时间抢购两个
- 加@Transactional的事务注解
- 将秒杀订单存入redis,以用户id和商品id做key,判断订单是否重复
redisTemplate.opsForValue().set("order:"+user.getId()+":"+goods.getId(),seckillOrder);
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
if (seckillOrder!=null){
model.addAttribute("errmsg", RespBeanEnum.REPEAT_ERROR.getMessage());
return RespBean.error(RespBeanEnum.REPEAT_ERROR);
}
接口的优化
- 关键在于少查数据库,多用缓存,缓存处理高并发的能力远强于数据库
- redis预减库存,减少数据库的访问
- 内存标记,减少redis的访问
- 请求进入队列缓存,异步下单(RabbitMQ)
- 数据库优化:集群,分库分表
队列的使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
@Configuration
public class RabbitMQConfig {
@Bean
public Queue queue(){
return new Queue("queue",true);
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息"+msg);
rabbitTemplate.convertAndSend("queue",msg);
}
}
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues="queue")
public void receive(Object msg){
log.info("接收消息::"+msg);
}
}
- 生产者→交换机→队列→消费者
- 交换机模式
- fanout(发布订阅模式,广播模式):消息能够被多个队列同时接收
@Configuration
public class RabbitMQConfig {
private static final String QUEUE01 = "queue_fanout01";
private static final String QUEUE02 = "queue_fanout02";
private static final String EXCHANGE = "fanoutExchange";
@Bean
public Queue queue(){
return new Queue("queue",true);
}
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange(EXCHANGE);
}
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(fanoutExchange());
}
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(fanoutExchange());
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(Object msg){
log.info("发送消息"+msg);
rabbitTemplate.convertAndSend("fanoutExchange","",msg);
}
}
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues="queue")
public void receive(Object msg){
log.info("接收消息::"+msg);
}
@RabbitListener(queues = "queue_fanout01")
public void receive01(Object msg){
log.info("QUEUE01接收消息::"+msg);
}
@RabbitListener(queues = "queue_fanout02")
public void receive02(Object msg){
log.info("QUEUE02接收消息::"+msg);
}
}
- direct
- 设置路由键,通过匹配路由键将消息发给不同的队列
- 路由键不匹配的消息默认会丢失
@Configuration
public class RabbitMQConfig {
private static final String QUEUE01 = "queue_direct01";
private static final String QUEUE02 = "queue_direct02";
private static final String EXCHANGE = "directExchange";
private static final String ROUTINGKEY01 = "queue.red";
private static final String ROUTINGKEY02 = "queue.green";
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange(EXCHANGE);
}
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(directExchange()).with(ROUTINGKEY01);
}
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(directExchange()).with(ROUTINGKEY02);
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send01(Object msg){
log.info("发送red消息"+msg);
rabbitTemplate.convertAndSend("directExchange","queue.red",msg);
}
public void send02(Object msg){
log.info("发送green消息"+msg);
rabbitTemplate.convertAndSend("directExchange","queue.green",msg);
}
}
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues="queue")
public void receive(Object msg){
log.info("接收消息::"+msg);
}
@RabbitListener(queues = "queue_direct01")
public void receive01(Object msg){
log.info("QUEUE01接收消息::"+msg);
}
@RabbitListener(queues = "queue_direct02")
public void receive02(Object msg){
log.info("QUEUE02接收消息::"+msg);
}
}
- topic
- *匹配一个,#匹配两个
- 如果同一个队列匹配上了多次,只向它发送一次消息
- 一个都匹配不到就丢弃消息
- topic模式既能实现fanout也能实现direct
@Configuration
public class RabbitMQConfig {
private static final String QUEUE01 = "queue_topic01";
private static final String QUEUE02 = "queue_topic02";
private static final String EXCHANGE = "topicExchange";
private static final String ROUTINGKEY01 = "#.queue.#";
private static final String ROUTINGKEY02 = "*.queue.#";
@Bean
public Queue queue01(){
return new Queue(QUEUE01);
}
@Bean
public Queue queue02(){
return new Queue(QUEUE02);
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding binding01(){
return BindingBuilder.bind(queue01()).to(topicExchange()).with(ROUTINGKEY01);
}
@Bean
public Binding binding02(){
return BindingBuilder.bind(queue02()).to(topicExchange()).with(ROUTINGKEY02);
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send01(Object msg){
log.info("发送01消息"+msg);
rabbitTemplate.convertAndSend("topicExchange","queue.red.message",msg);
}
public void send02(Object msg){
log.info("发送01,02消息"+msg);
rabbitTemplate.convertAndSend("topicExchange","red.queue.green",msg);
}
}
@Service
@Slf4j
public class MQReceiver {
@RabbitListener(queues = "queue_topic01")
public void receive01(Object msg){
log.info("QUEUE01接收消息::"+msg);
}
@RabbitListener(queues = "queue_topic02")
public void receive02(Object msg){
log.info("QUEUE02接收消息::"+msg);
}
}
redis预减库存
- redis库存为0之后,其他请求都不会再去数据库查询
- 项目初始化的时候就加载库存到redis中
@Controller
@RequestMapping("/seckill")
public class SecKillController implements InitializingBean {
@Autowired
private IUserService userService;
@Autowired
private IGoodsService goodsService;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private IOrderService orderService;
@Autowired
private RedisTemplate redisTemplate;
@ResponseBody
@RequestMapping(value="/doSeckill",method = RequestMethod.POST)
public RespBean doSecKill(Long goodsId, Model model, HttpServletRequest request, HttpServletResponse response){
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
if(user==null){
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
GoodsVo goods = goodsService.findGoodsByGoodsId(goodsId);
ValueOperations valueOperations = redisTemplate.opsForValue();
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
if (seckillOrder!=null){
return RespBean.error(RespBeanEnum.REPEAT_ERROR);
}
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
if(stock<0){
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
Order order = orderService.sekill(user,goods);
return RespBean.success(order);
return null;
}
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> list = goodsService.findGoodsVo();
if(CollectionUtils.isEmpty(list)){
return;
}
for (GoodsVo goodsVo : list) {
redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());
}
}
}
给Redis增加一个内存标记,减少redis的访问
private Map<Long,Boolean> EmptyStockMap = new HashMap<>();
- 内存满了之后(即Redis内存清零)后将map设为true,之后读取redis前先判断一下,如果为true就不读redis了
先将请求加载到队列中,显示排队中的状态
- 所有秒杀请求都先存在队列中,此时给前端返回一个排队中的状态
- 消费者挨个接收消息,并判断库存,进行处理,生成订单,减小库存,此时只能生成10个订单
@Configuration
public class RabbitMQConfig {
private static final String QUEUE = "seckillQueue";
private static String EXCHANGE = "seckillExchange";
@Bean
public Queue queue(){
return new Queue(QUEUE);
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
}
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendSeckillMessage(String msg){
log.info("发送消息:"+msg);
rabbitTemplate.convertAndSend("seckillExchange","seckill.message",msg);
}
}
@Service
@Slf4j
public class MQReceiver {
@Autowired
private IGoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IOrderService orderService;
@RabbitListener(queues="seckillQueue")
public void receive(String msg){
log.info("接收到消息"+msg);
SeckillMessage seckillMessage = JSON.parseObject(msg, SeckillMessage.class);
Long goodsId = seckillMessage.getGoodsId();
User user = seckillMessage.getUser();
GoodsVo goodsVo = goodsService.findGoodsByGoodsId(goodsId);
if(goodsVo.getStockCount()<1){
return;
}
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);
if (seckillOrder!=null){
return;
}
orderService.sekill(user,goodsVo);
}
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));
return RespBean.success(0);
轮询判断是否真的秒杀成功
- 前端页面收到正在排队中后,回去查找订单现在的状态
- 如果查到订单了,说明抢购成功
- 如果没查到,而且商品没有了,说明抢购失败
- 如果商品还有,则返回还在排队中
- 如果前端得到的还是排队中,就再次调用这个方法,即进行轮询
function doSecKill(){
$.ajax({
url: '/seckill/doSeckill',
type: 'POST',
data:{
goodsId:$("#goodsId").val()
},
success:function (data){
if(data.code==200){
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function (){
layer.msg("客户端请求错误");
}
})
}
function getResult(goodsId){
g_showLoading();
$.ajax({
url:"/seckill/getResult",
type:"GET",
data:{
goodsId:goodsId,
},
success:function(data){
if(data.code==200){
var result = data.obj;
if(result<0){
layer.msg("对不起,秒杀失败!")
}else if(result==0){
setTimeout(function (){
getResult(goodsId);
},50);
}else{
layer.confirm("恭喜你,秒杀成功!查看订单吗?",{btn:["确定","取消"]},
function(){
window.location.href="/static/orderDetail.htm?orderId="+result;
},
function(){
layer.close();
})
}
}else{
layer.msg(data.message);
}
},
error:function (){
layer.msg("客户端请求错误");
}
})
}
@Service
public class SeckillOrderServiceImpl extends ServiceImpl<SeckillOrderMapper, SeckillOrder> implements ISeckillOrderService {
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Long getResult(User user, Long goodsId) {
QueryWrapper<SeckillOrder> wrapper = new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId);
SeckillOrder seckillOrder = seckillOrderMapper.selectOne(wrapper);
if(null != seckillOrder){
return seckillOrder.getOrderId();
}else if(redisTemplate.hasKey("isStockEmpty:"+goodsId)){
return -1L;
}else{
return 0L;
}
}
}
redis锁
- 一个线程去操作redis就会占位,别的线程就无法操作,这个线程执行完之后删除锁
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testLock01() {
ValueOperations valueOperations = redisTemplate.opsForValue();
Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
if(isLock){
valueOperations.set("name","mgy");
String name = (String) valueOperations.get("name");
System.out.println(name);
redisTemplate.delete("k1");
}
else{
System.out.println("有线程在使用,请稍后");
}
}
- 使用lua脚本能先获取锁,然后判断锁的值是否一致,然后再进行删除(保证每次只删自己的锁,不删别人的锁)
安全优化
隐藏接口地址
- 先获得一个接口地址,再进行秒杀,根据用户和商品给出唯一的接口地址,让其他人不能使用
- 将以用户和商品为key,地址为value的数据存入Redis
function getSeckillPath(){
var goodsId = $("#goodsId").val();
console.log(goodsId);
g_showLoading();
$.ajax({
url:"/seckill/path",
type:'GET',
data:{
goodsId:goodsId,
},
success:function(data){
if(data.code==200){
var path = data.obj;
doSecKill(path);
}else{
layer.msg(data.message);
}
},
error:function(){
layer.msg("客户端请求错误");
}
})
}
function doSecKill(path){
$.ajax({
url: '/seckill/'+path+'/doSeckill',
type: 'POST',
data:{
goodsId:$("#goodsId").val(),
},
success:function (data){
if(data.code==200){
// window.location.href="/static/orderDetail.htm?orderId="+data.obj.id;
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function (){
layer.msg("客户端请求错误");
}
})
}
@RequestMapping(value = "/path",method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(Long goodsId, HttpServletRequest request, HttpServletResponse response){
System.out.println("获得路径");
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
if(user==null){
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
String str = orderService.createPath(user,goodsId);
System.out.println(str);
return RespBean.success(str);
}
@Override
public String createPath(User user, Long goodsId) {
String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
redisTemplate.opsForValue().set("seckillPath:"+user.getId()+":"+goodsId,str,60, TimeUnit.MINUTES);
return str;
}
@ResponseBody
@RequestMapping(value="/{path}/doSeckill",method = RequestMethod.POST)
public RespBean doSecKill(Long goodsId, @PathVariable String path, HttpServletRequest request, HttpServletResponse response){
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
if(user==null){
return RespBean.error(RespBeanEnum.LOGIN_ERROR);
}
GoodsVo goods = goodsService.findGoodsByGoodsId(goodsId);
ValueOperations valueOperations = redisTemplate.opsForValue();
boolean check = orderService.checkPath(user,goodsId,path);
@Override
public boolean checkPath(User user, Long goodsId, String path) {
if(user==null||goodsId<0|| StringUtils.isEmpty(path)){
return false;
}
String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() + ":" + goodsId);
return path.equals(redisPath);
}
添加验证码
- 增加机器抢购难度
- 减少并发
- 将用户id和商品id做为key,验证码的值作为参数存进Redis里
- 只有输入正确验证码才能通过redis的校验
- 来自gitee上的开源代码,不写了
接口限流
- 用redis记录次数,有100个缓存之后就无法通过,一分钟到了部分缓存失效,就又可以存入
ValueOperations valueOperations = redisTemplate.opsForValue();
String uri = request.getRequestURI();
Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
if(count==null){
valueOperations.set(uri+":"+user.getId(),1,5,TimeUnit.SECONDS);
}else if(count<5){
valueOperations.increment(uri+":"+user.getId());
}else{
return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REACHED);
}
- 限流控制在最大能承受的QPS的70%-80%
- 这个方法的问题在于临界失效前后如果有大量请求的话,还是会超过QPS
- 漏桶算法:一部分进一部分出,通常用队列实现,但是可能导致桶被装满,太少会造成资源的浪费
- 令牌算法:以恒定的速度生成令牌,放进令牌桶里,如果桶满了就丢弃令牌,一个请求出现后要去拿令牌,拿到令牌才能够被执行
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TIsm9Hf-1648091808661)(.doc.markdown_images/9d58a6b5.png)]
- redis优势:具有递增原子性,能够设计失效时间
通用接口限流
- threadLocal:每个线程绑定自己的值,不会造成用户信息紊乱。相当于每个线程中一个存放私有数据的盒子
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int second();
int maxCount();
boolean needLogin() default true;
}
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
User user = userService.getUserByCookie(ticket,request,response);
UserContext.setUser(user);
if(handler instanceof HandlerMethod){
HandlerMethod hm = (HandlerMethod) handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit==null){
return true;
}
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin){
if(user==null){
return false;
}
key+=":"+user.getId();
}
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer count = (Integer) valueOperations.get(key);
if(count==null){
valueOperations.set(key,1,second, TimeUnit.SECONDS);
}else if(count<maxCount){
valueOperations.increment(key);
}else{
return false;
}
}
return false;
}
}
秒杀主流方案分析
需要注意的问题
- 高并发,刷接口等黑客请求,超出负载
- 高并发时导致的超卖
- 该负载情况下下单成功率的保障
网关限流
- 黑名单,放在内存里
- 多次请求,redis缓存重复
- 没有预约(没有令牌)
- 预约可以提前发放部分令牌
- redission加分布式锁,redis集群
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit==null){
return true;
}
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin){
if(user==null){
return false;
}
key+=":"+user.getId();
}
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer count = (Integer) valueOperations.get(key);
if(count==null){
valueOperations.set(key,1,second, TimeUnit.SECONDS);
}else if(count<maxCount){
valueOperations.increment(key);
}else{
return false;
}
}
return false;
}
}
秒杀主流方案分析
需要注意的问题
- 高并发,刷接口等黑客请求,超出负载
- 高并发时导致的超卖
- 该负载情况下下单成功率的保障
网关限流
- 黑名单,放在内存里
- 多次请求,redis缓存重复
- 没有预约(没有令牌)
- 预约可以提前发放部分令牌
- redission加分布式锁,redis集群