2021-11-03

秒杀处理超卖问题,第一种,利用悲观锁处理,(处理的书数据库)

没处理之前(大概有四百个线程抢到)(总共10000个线程)(总共有10个产品数量)

这个直观,在上面加一个成员变量,
private static int count=0;

在查询语句里加一个for update
但是还是会超卖,因为一个业务层操作了几个不同的dao层方法,所以要让他们在一个事务里,保持原子性

悲观锁虽然能控制住,但是效率低,性能不高
缺点:会有很多的无效请求,我们只有10个有效请求,剩下的9000多个无用请求也会去访问数据库,所以可以需要拦截掉,以此来保护数据库
乐观锁来防止超卖的问题
用预扣存的方式来解决超卖

定时任务从数据库查询即将开始的秒杀活动,然后讲秒杀活动的信息发一份到Redis,redis里面需要怎么存数据,关键存的是秒杀商品的数量,key-----》miaosha:product:1(1是活动ID) value---->是活动的产品数量 存数量先要判断get数量是否大于0,然后还要递减,redis操作一个操作时原子性,两个操作就不是原子性了,所以可以写一个lua脚本,自己写一个命令,将两个操作组合成一个,这样的话直接存数量不太好!此外,可以换一种方式存,redis有个数据结构是List结构,list结构可以弹数据
即value—》list(1001,1002),存产品的ID,抢一个弹一个
另外里面还要存用户的信息:key—》miaosha:user:1(活动ID) value—》set(101,102)存用户的标识,比如用户的ID, 用什么数据结构,一般一个活动只能允许用户抢到一个商品,既不能重复抢购,,所以用redis的set结构来存用户的标识
客户端抢购商品,redis里面开始减数量,并且记录抢购成功的用户信息,通过mq向订单发送消息,并且同步到数据库

第一步:写定时任务

/**

  • 写一个任务管理器,为了将即将开始的活动保存到redis里

  • 首先判断时间,当前的时间

  • 查询可以进行秒杀的活动还有一个方法写清理redis
    */
    @Component
    public class MiaoshaTask {
    @Resource(name = “myStringRedisTemplate”)
    private RedisTemplate<String,Object> redisTemplate;
    @Resource(name = “myIntegerRedisTemplate”)
    private RedisTemplate<String,Integer> IntegerRedisTemplate;
    @Autowired
    private MiaoshaMapper miaoshaMapper;

    @Scheduled(cron = “0/10 * * * * ?”)
    public void queryCanStartKillProduct(){
    //查询即将开始的秒杀活动
    //select * from miaosha where flag=1 and miaosha_check=1 and status=0 and now() between start_time and end_time
    List list=miaoshaMapper.queryStartActive();
    if (list!=null && !list.isEmpty()){
    for (Miaosha miaosha : list) {
    //构建一个秒杀活动信息的key
    String key = new StringBuilder(“miaosha:product:”).append(miaosha.getId()).toString();
    //得到秒杀商品数量
    // for (Integer i = 0; i < miaosha.getCount(); i++) {
    // //数量有多少个,就保存多少个商品ID在List中
    // redisTemplate.opsForList().leftPush(key,miaosha.getProductId());
    // }
    //一个一个添加需要多次访问数据库,所以批量操作,只访问一次数据库,有两种方式,一种是redis的流水线,也就是管道,还有一种是集合
    //管道略,看笔记,只写第二种
    Collection ids=new ArrayList<>(miaosha.getCount());
    for (Integer i = 0; i < miaosha.getCount(); i++) {
    ids.add(miaosha.getProductId());
    }
    IntegerRedisTemplate.opsForList().leftPushAll(key,ids);
    //把未开启秒杀状态改为开启状态 0—》1
    miaosha.setStatus(“1”);
    miaoshaMapper.updateByPrimaryKeySelective(miaosha);
    //存秒杀活动本身;
    String activeKey=new StringBuilder(“miaosha:active:”).append(miaosha.getId()).toString();
    redisTemplate.opsForValue().set(activeKey,miaosha);
    }
    System.out.println(“redis初始化信息成功”);
    }

    }
    /**

    • 查询结束的秒杀活动,并将redis的数据进行清理

    • 判断时间

    • 当前时间大于结束时间就把状态改为2
      */
      @Scheduled(cron = “0/10 * * * * ?”)
      public void queryEndKillProduct() {
      List list= miaoshaMapper.queryEndKillProduct();
      if (list!=null && !list.isEmpty()){
      for (Miaosha miaosha : list) {
      //构建一个秒杀活动信息的key
      String key = new StringBuilder(“miaosha:product:”).append(miaosha.getId()).toString();
      //清理商品数量信息
      IntegerRedisTemplate.delete(key);
      //清理秒杀活动本身;
      String activeKey=new StringBuilder(“miaosha:active:”).append(miaosha.getId()).toString();
      redisTemplate.delete(activeKey);
      //把开启秒杀状态改为已结束状态 1—》2
      miaosha.setStatus(“2”);
      miaoshaMapper.updateByPrimaryKeySelective(miaosha);

       }
       System.out.println("清理成功");
      

      }
      }
      }

redis的工具类,改成集合的方式,value 不能是对象
package com.yw.miaosha.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Bean(name="myStringRedisTemplate")
public RedisTemplate<String,Object> getTemplate(RedisConnectionFactory connectionFactory){
    RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    //key的序列化
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    //我们的 value 使用什么方式来进行序列化,因为 value是任意的对象,所以我们使用 json
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//创建一个解析为 json 的序列化对象
    ObjectMapper objectMapper = new ObjectMapper();//因为 jackson 中是使用ObjectMapper来进行序列化的,所以我们需要设置给ObjectMapper
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);//设置所有非 final 修饰的变量都可以被序列化
    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);//指定我们的 objectmapper

    //设置 value 的序列化方式
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//设置值的序列化方式为 json
    redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
}
//方法名不能重复啊白痴啊白痴
@Bean(name="myIntegerRedisTemplate")
public RedisTemplate<String,Integer> getIntegerTemplate(RedisConnectionFactory connectionFactory){
    RedisTemplate<String,Integer> redisTemplate=new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    //key的序列化
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    return redisTemplate;
}

}

第二步:业务层的处理

/**

  • 判断活动状态
  • 1未开启
  • 2已经结束
  • 3用户抢购过了,不能在抢购
  • 4商品被抢购完了
  • 5能进行抢购
  • reids版本(预扣库存方案)实现秒杀处理逻辑
  • @param userId
  • @param id
  • @return
    */
    @Override
    public ResultBean kill(Integer userId, Integer id) {
    //通过活动Id获取秒杀活动的信息
    // Miaosha miaosha = miaoshaMapper.selectByPrimaryKey(id);
    String activeKey=new StringBuilder(“miaosha:active:”).append(id).toString();
    Miaosha miaosha = (Miaosha) redisTemplate.opsForValue().get(activeKey);
    if (“0”.equals(miaosha.getStatus())){
    throw new MiaoshaException(“秒杀活动还未开始,请等待。。。”);
    }
    if (“2”.equals(miaosha.getStatus())){
    throw new MiaoshaException(“秒杀活动已经结束,请下次吧”);
    }
    //通过redis查找用户是抢购过,构建key
    String key=new StringBuilder(“miaosha:user:”).append(miaosha.getId()).toString();
    //用户的信息保存在set数据结构中
    Boolean member = redisTemplate.opsForSet().isMember(key, userId);
    if (member){
    //已经抢购过了,不能够重复抢购
    throw new MiaoshaException(“已经抢购过了,不能够重复抢购”);
    }
    //商品是否抢购完了
    String productKey=new StringBuilder(“miaosha:product:”).append(miaosha.getId()).toString();
    //id为空,说明抢购完了
    Integer productId = (Integer) redisTemplate.opsForList().leftPop(productKey);
    if (productId==null){
    //已经没有数量了
    throw new MiaoshaException(“商品抢购完了,下次吧”);
    }
    //有数量,能进行抢购,抢购成功
    //存用户Id
    redisTemplate.opsForSet().add(key,userId);
    return new ResultBean(“200”,“抢购成功”);
    }

第三步:将成功抢购成功的用户信息发送给订单
导依赖

自动注入模板
@Autowired
private RabbitTemplate rabbitTemplate;
首先:创建订单表,订单表里需要用户信息,商品名称,订单编号,也可以直接创建一个Map集合,将数据放进去
@Override
public ResultBean kill(Integer userId, Integer id) {
//通过活动Id获取秒杀活动的信息
// Miaosha miaosha = miaoshaMapper.selectByPrimaryKey(id);
String activeKey=new StringBuilder(“miaosha:active:”).append(id).toString();
Miaosha miaosha = (Miaosha) redisTemplate.opsForValue().get(activeKey);
if (“0”.equals(miaosha.getStatus())){
throw new MiaoshaException(“秒杀活动还未开始,请等待。。。”);
}
if (“2”.equals(miaosha.getStatus())){
throw new MiaoshaException(“秒杀活动已经结束,请下次吧”);
}
//通过redis查找用户是抢购过,构建key
String key=new StringBuilder(“miaosha:user:”).append(miaosha.getId()).toString();
//将用户的信息保存在set数据结构中,如果能保存,代表没有抢购过,会返回1,有这个用户的信息,会返回0
long result = redisTemplate.opsForSet().add(key,userId);
System.err.println(result);
if (result0){
//已经抢购过了,不能够重复抢购
throw new MiaoshaException(“已经抢购过了,不能够重复抢购”);
}
//商品是否抢购完了
String productKey=new StringBuilder(“miaosha:product:”).append(miaosha.getId()).toString();
//id为空,说明抢购完了
Integer productId = (Integer) IntegerRedisTemplate.opsForList().leftPop(productKey);
//System.err.println(productId);
if (productId
null){
//已经没有数量了,不知道为什么,所有代码要写在抛出异常之前
//当商品Id为空的时候,加一个用户删除一个用户,程序是向下执行的,一定要记得,不会debug一下
redisTemplate.opsForSet().remove(key,userId);
//将状态设置为2,已结束活动
miaosha.setStatus(“2”);
miaoshaMapper.updateByPrimaryKeySelective(miaosha);

        throw new MiaoshaException("商品抢购完了,下次吧");

    }
    //5,有数量,能进行抢购,抢购成功
    //存用户Id

// redisTemplate.opsForSet().add(key,userId);
//SSSS指毫秒
SimpleDateFormat simpleDateFormat=new SimpleDateFormat(“yyyyMMddhhmmssSSSS”);
String orderNo = simpleDateFormat.format(System.currentTimeMillis()).toString();
//给订单系统发送消息,然后订单系统在往数据库表中添加数据
//关键的数据 user_id product—id count orderNo product_price
Map<String,Object> paramMap=new HashMap<>();
paramMap.put(“user_id”, userId);
paramMap.put(“product_id”, miaosha.getProductId());
paramMap.put(“count”, 1);
paramMap.put(“product_price”, miaosha.getProductPrice());
//订单编号需要唯一
//有四种方式:UUID,时间戳,Redis的自增,雪花算法
//这里使用时间戳,UUId可读性不好
paramMap.put(“orderNo”, orderNo+userId);

 rabbitTemplate.convertAndSend("order-exchange", "order.create",paramMap);

    return new ResultBean("200","抢购成功");
}

}
订单系统接收消息
package com.yw.shoporder.listener;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;
/**

  • 监听器,秒杀系统,将抢购到的用户的信息发送订单系统
  • 发送的内容在map集合里
  • reids用的set数据类型,key:miaoshao:user:id value:userid
    */
    @Component
    public class OrderListener {
    @RabbitListener(queues = “order-queue”)
    public void receive(Map<String,Object> map){
    System.out.println(map);
    System.out.println(map.get(“orderNo”));
    }
    }
    秒杀系统和订单系统交互

解决问题:秒杀订单量暴增的问题
办法:1:订单系统加集群
2、限流: mq中的QOS+手动ACK

application.yml文件里的配置
listener:
simple:
prefetch: 1 #表示每次rabbitmq给我发送几个消息
acknowledge-mode: manual #代表手动ack
订单系统的监听器的代码,实现接收秒杀系统抢购成功用户的消息并限流
package com.yw.shoporder.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**

  • 监听器,秒杀系统,将抢购到的用户的信息发送订单系统
  • 发送的内容在map集合里
  • reids用的set数据类型,key:miaoshao:user:id value:userid
    */
    @Component
    public class OrderListener {
    @RabbitListener(queues = “order-queue”)
    public void receive(Map<String,Object> map, Channel channel, Message message){
    System.out.println(map);
    System.out.println(map.get(“orderNo”));
    try {
    System.out.println(“模拟订单系统生成订单表和订单明细信息。。。。。。”);
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    //手动ack
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
    channel.basicAck(deliveryTag,false);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    安全加固
    隐藏秒杀地址:
    即使是内部员工,也无法提前知道秒杀接口的地址,因为最终的秒杀地址是根据用户信息来随机生成的。
    而对于攻击者来说,秒杀地址是随机生成的且一分钟内有效(甚至可以设置一次性的),意味着后续无法持续使用,每次都需要重新获取新的秒杀地址,从而加大其攻击的成本。
    秒杀开始之前,先去请求接口获取秒杀地址,如果未到时,则无法获取到秒杀地址。

如何配置动态秒杀地址、
1、点击秒杀获取请求地址,然后Controller通过业务层生成UUid的随机地址在redis保存并返回,用户带着生成的path地址访问秒杀页面,传过来的path如果和redis里的一致,即可以访问
业务层代码:
@Override
public ResultBean getPath(Integer userId, Integer id) {
//先判断活动开没开始
String activeKey=new StringBuilder(“miaosha:active:”).append(id).toString();
Miaosha miaosha = (Miaosha) redisTemplate.opsForValue().get(activeKey);
if (“0”.equals(miaosha.getStatus())){
throw new MiaoshaException(“秒杀活动还未开始,请等待。。。”);
}
if (“2”.equals(miaosha.getStatus())){
throw new MiaoshaException(“秒杀活动已经结束,请下次吧”);
}
//动态生成随机的path
String path= UUID.randomUUID().toString();
//存到Redis,需要拼一个key
String pathKey = new StringBuilder(“miaosha:”).append(userId).append("😊.append(id).toString();
//存到redis
redisTemplate.opsForValue().set(pathKey,path);
//设置生存时间,1分钟
redisTemplate.expire(pathKey,1, TimeUnit.MINUTES);
return new ResultBean(“200”,path);
}
在kill的方法里在添加一个变量
@Override
public ResultBean kill(Integer userId, Integer id, String path) {
//判断path是否合法
//从redis里查一下
String pathKey = new StringBuilder(“miaosha:”).append(userId).append("😊.append(id).toString();
Object o = redisTemplate.opsForValue().get(pathKey);
if (o==null){
throw new MiaoshaException(“秒杀地址不合法”);
}
//一次性有效
redisTemplate.delete(pathKey);
//通过活动Id获取秒杀活动的信息
// Miaosha miaosha = miaoshaMapper.selectByPrimaryKey(id);
String activeKey=new StringBuilder(“miaosha:active:”).append(id).toString();
Miaosha miaosha = (Miaosha) redisTemplate.opsForValue().get(activeKey);
if (“0”.equals(miaosha.getStatus())){
throw new MiaoshaException(“秒杀活动还未开始,请等待。。。”);
}
Controller处理
/**

  • 安全,为了防止内部员工知道接口访问,所以生成动态的

  • 首先,客户端发送请求到生成pat接口。接口返回动态生成的path(前提:到了秒杀时间+用户已经登录)

  • key—>miaosha:userId,secKillId value—>path

  • 生存时间一分钟,也可以一次性,用完就丢

  • 拿回返回的path,在构建一个新的请求地址,请求秒杀接口

  • 判断是否合法

  • 只要判断redis中是否存着path,如果没有就是不合法的请求
    */
    @RequestMapping("/getPath")
    @ResponseBody
    public ResultBean getPath(Integer userId,Integer id){

    try {
    return miaoshaService.getPath(userId,id);
    } catch (MiaoshaException e) {
    return new ResultBean(“400”,e.getMessage());
    }
    }
    kill方法里加Path变量
    /**

    • 后台处理,
    • 第一,需要知道是哪个用户在抢购
    • 第二,秒杀的并发的处理
      */
      @RequestMapping("/kill/{path}")
      @ResponseBody
      public ResultBean kill(@PathVariable String path, Integer userId, Integer id){
      // 判断Path的合法性
      try {
      //System.err.println(userId);
      return miaoshaService.kill(userId,id,path);
      } catch (MiaoshaException e) {
      //e.getMessage()是业务层抛出的信息
      return new ResultBean(“400”,e.getMessage());
      }
      }

10.2,添加验证码
点击秒杀之前,必须填写验证码
严格来说,没有无法破解的验证码,添加验证码,也是为了加大攻击的成本
1,添加依赖

com.github.penggle
kaptcha
2.3.2

2,配置生成验证码的Bean
@Bean
public DefaultKaptcha getDefaultKaptche(){

DefaultKaptcha defaultKaptcha = new DefaultKaptcha();

Properties properties = new Properties();
// 图片边框
properties.setProperty(“kaptcha.border”, “yes”);
// 边框颜色
properties.setProperty(“kaptcha.border.color”, “106,180,90”);
// 字体颜色
properties.setProperty(“kaptcha.textproducer.font.color”, “red”);
// 图片宽
properties.setProperty(“kaptcha.image.width”, “110”);
// 图片高
properties.setProperty(“kaptcha.image.height”, “40”);
// 字体大小
properties.setProperty(“kaptcha.textproducer.font.size”, “30”);
// session key
properties.setProperty(“kaptcha.session.key”, “code”);
// 验证码长度
properties.setProperty(“kaptcha.textproducer.char.length”, “4”);
// 字体
properties.setProperty(“kaptcha.textproducer.font.names”, “宋体,楷体,微软雅黑”);

defaultKaptcha.setConfig(new Config(properties));
return defaultKaptcha;
}
3,提供一个获取验证码的接口
@Autowired
private DefaultKaptcha defaultKaptcha;



@RequestMapping("/getCode")
@ResponseBody
public void getCode(HttpServletResponse response){


//生成验证码 - 保存到服务器
String text = defaultKaptcha.createText();


//根据验证码生成图片
BufferedImage image = defaultKaptcha.createImage(text);


//将验证码图片写回客户端
try {
ImageIO.write(image, “jpg”, response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}

输入验证码,点击立即抢购,把验证码带到后端,redis里面现存一份,controller生成验证码,存到redis里,哪个用户对应哪儿个验证码,能够抢购,说明已经登录。
11,限流防刷
防止用户在短期内发送大量的请求,将恶意请求拦截在上层
mq的限流是系统和 系统之间的,现在是客户端和系统之间的限流

12,数据的一致性考虑
分布式事务,采用MQ实现柔性事务,实现最终的数据一致性

embedcpp-2021-03是一个有关嵌入式C++编程的课程,于2021年3月举办。嵌入式C++编程是指在嵌入式系统中使用C++编程语言进行开发的一种方法。 在嵌入式系统中,资源通常是有限的,例如处理器速度、内存容量和存储空间等。因此,使用C++编程语言可以提供更高的灵活性和效率,帮助开发人员充分利用有限的资源。C++在嵌入式系统中的应用范围广泛,例如物联网设备、汽车电子和工业自动化等领域。 embedcpp-2021-03课程旨在向学员介绍嵌入式C++编程的基础知识和技巧。课程内容通常包括以下方面: 1. C++语法和特性:介绍C++的基本语法、面向对象编程和泛型编程等概念,以及C++11、C++14和C++17的一些新特性。 2. 嵌入式系统概述:了解嵌入式系统的基本特点、硬件和软件组成,以及与传统桌面开发的区别。 3. 低级编程:学习如何与硬件交互,包括使用寄存器、配置外设和处理中断等。还可以介绍使用汇编语言优化性能的技巧。 4. 内存管理:探讨嵌入式系统中的内存管理技术,包括堆栈和堆的使用、动态内存分配和对象生命周期管理等。 5. 实时操作系统(RTOS):介绍嵌入式系统中常用的实时操作系统,如FreeRTOS和µC/OS等,学习如何使用RTOS进行任务调度和资源管理。 除了理论知识,embedcpp-2021-03课程通常还包括实际的项目练习,以帮助学员将所学知识应用于实际场景。通过该课程,学员可以了解嵌入式C++编程的基础概念和实践技巧,为嵌入式系统开发提供了一定的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值