商城秒杀项目开发目录

基于慕课网实战上完成商城的秒杀项目,视频链接

项目框架搭建

1、Spring boot 环境搭建
2、集成Themyleaf,Result结果封装
3、集成MybatisPlus、druid
4、集成Jedis+Redis,通用缓存Key连接

完成模块

在这里插入图片描述

1、md5加密

在这里插入图片描述
共用到两次md5加密技术,第一次md5加密是前端传给后端的时候,通过md5 + salt,可以防止用户密码在传输过程中暴露。第二次加密是在存储入数据库的时候,也是md5 + salt,防止数据库被盗黑客获取md5秘钥+md5迫切方法,进行破解。

2、数据库连接池

使用springboot自带的连接池hikari。springboot的配置参数以及连接池的配置参数如下。

spring:
  themeleaf:
    # 关闭缓存
    cache: false
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seckill?userUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    # 连接池,默认自带的连接池
    hikari:
      pool-name: DateHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲链接存活最大时间,默认是10分钟(60000)
      idle-timeout: 1800000
      # 最大连接数,默认是10
      maximum-pool-size: 10
      # 从连接池返回的连接自动提交
      auto-commit: true
      # 连接最大存活时间,0表示永久,默认30分钟
      max-lifetime: 1800000
      # 连接超时时间,默认是30秒
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-test-query: SELECT 1
mybatis-plus:
  # 配置xml映射的位置
  mapper-locations: classpath*:/mapper/*Mapper.xml
  # 配置mybatis数据返回类型别名(默认别名是类名)
  type-aliases-package: com.example.shoppingscklill.pojo


logging:
  level:
    com.example.shoppingscklill.mapper: debug

3、spring validation

springboot自带的校验包,可以节省很多校验代码。

<!--        validation springboot 校验-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

使用: 加入@Valid注解

 @RequestMapping("/doLogin")
    public RespBean doLogin(@Valid LoginVo loginVo){
        return itUserService.doLogin(loginVo);

代码块加上如下注解

@Data
public class LoginVo {
    @NotNull
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min = 32)
    private String password;
}

自定义注解@IsMobile

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    boolean required() default true;

    String message() default "{手机格式校验错误}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotNull[] value();
    }
}
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {

    private boolean required = false;
    //初始化
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(required){
            ValidatorUtil.isMobile(value);
        }else{
            if(StringUtils.isEmpty(value)){
                return true;
            }else{
                return ValidatorUtil.isMobile(value);
            }
        }

        return false;
    }
}

但是注解@IsMobile这样运行的时候,只是会抛出异常,不会捕获异常,这里要写自定义异常来捕获。

4、springboot的异常处理

链接
ControllerAdvice和ErrorController的用法。在Java中异常可以分为Error和Exception,@ControllerAdvice和ExceptionHandler()的组合用来处理系统中的Exception,ErrorController处理系统中的Error(如404)。

针对3抛出的绑定异常,直接使用 @RestControllerAdvice(直接返回json)来处理就可以了
编写异常类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GlobalException extends RuntimeException{
    private ResBeanEnum respBeanEnum;

}

编写ExceptionHandler

public class GlobalExceptionHandler {

    //需要处理的异常类
    @ExceptionHandler(Exception.class)
    public RespBean ExceptionHandler(Exception e){
        if(e instanceof GlobalException){
            GlobalException ex = (GlobalException) e;
            return RespBean.failure(ex.getRespBeanEnum());
            //抛出绑定异常
        }else if(e instanceof BindException){
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.failure(ResBeanEnum.BIND_error);
            respBean.setMessage("参数校对异常:" + ex.getBindingResult().getAllErrors());
        }
        return RespBean.failure(ResBeanEnum.error);
    }
}

5、mysql数据表的设计

物品表
订单表
用户表
秒杀商品表
秒杀订单表

6、redis简单操作

首先五种基本类型的增删改查

//string
set name zhangsan
get name
mset age 18 addr shanghai // 批量增加
//hash命令:
hset user name zhangsan //redis_key hash_key value
hget user name			//redis_key hash_key
hmset user age 19 addr 背beijing 
hmget user age addr
hgetall user
hdel user age
//list命令 有左有后
lpush students zhangsan lisi //list_name value value
rpush ...
lrange students 0 1 // 拿列表坐标[0,1]间的数据
llen students
lrem students 1 lisi //从左边删除1个lisi,可以删除多个值
//set 无序不可重复
sadd letters aaa bbb ccc ddd eee //添加
smembers //遍历所有值
scard letters//获取长度
srem letters aaa //删除
//sorted set 有序不可重复 
zdd score 1 zhangsan 5 lisi 3 wangwu //根据分数来排序
zrange score 0 3//拿数据
zcard score //获取长度

del 通用的删除命令
nx |xx //key不存在/存在的时候才能设置成功
set code test xx //返回(nil),因为之间code不存在,设置失败
set code test ex 10 //不存在的key设置10s时间 ,ex秒,px毫秒
expire xx 10 设置xx过期时间10s //已经存在的key设置失效时间
ttl key //查看key剩余时间

redis的序列化:

@Configuration
public class redisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){

        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        //key的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value的序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // Hash序列化
        redisTemplate.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
        // value的序列化
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }

}
  //将用户信息存入redis中
        redisTemplate.opsForValue().set("user:"+ticket,user);
//取值
TUser user = (TUser) redisTemplate.opsForValue().get("user:" + userTicket);

7、springboot实现分布式session

问题:
1、在分布式服务中,存在多台服务器,因此session需要存储在第三方(redis)中,使得多台服务器都能读到session。
2、每个页面都需要验证User的session信息,而每个类都验证一遍非常麻烦。因此引入mvc配置类的WebMvcConfigurer。
在这里插入图片描述

controller

    @RequestMapping("/toList")
    public String toList(Model model, TUser user){
//        if(StringUtils.isEmpty(ticket)){
//            return "login";
//        }
//        TUser user = userService.getUserByCookie(ticket, request, response);
//        if( null == user){
//            return "login";
//        }
        //传到前端,Model都是theMyLeaf控制类
        model.addAttribute("user",user);

        return "goodList";
    }
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private UserArgumentResolver userArgumentResolver;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private ITUserService userService;
    //条件判断
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> type = methodParameter.getParameterType();
        return type == TUser.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        String userTicket = CookieUtil.getCookieValue(request, "userTicket");
        if(userTicket == null){
            return null;
        }
        return userService.getUserByCookie(userTicket,request,response);
    }
}

8、秒杀活动设计

页面:
数据表:商品表、订单表、秒杀表、秒杀订单秒。

数据库的类型

int bigint 
float 
decimal(10,2) 10位数字,两位小数

date YYYY-MM-DD 
time HH:MM:SS
DATETIME  YYYY-MM-DD HH:MM:SS

varchar
text 长文本数据
longtext 极大文本数据

秒杀接口设计:
1、秒杀接口的在初始化的时候将商品存入redis中。

  • 继承initiaBean接口,实现afterProperties方法。项目启动就会运行。

2、收到请求,redis预见库存,若库存不足,直接返回失败,不需要查数据库。
3、请求入列,存入rabbitMq的队列中。(异步)
4、请求出列,生成订单,减少库存。(异步)
5、客户端轮询,是否秒杀成功。若卖完,存入redisKey的值,若之后获取到redisKey,则表示秒杀失败。(异步)

7、jmeter

开启1000个线程循环10次同时访问
QPS = 423 优化前
优化后:QPS = 2501

8、redis缓存

页面缓存、页面静态化

1、spring data redis

获取string类型变量

		ValueOperations valueOperations = redisTemplate.opsForValue();
		//Redis中获取页面,如果不为空,直接返回页面
		String html = (String) valueOperations.get("goodsDetail:" + goodsId);
		if (!StringUtils.isEmpty(html)) {
			return html;
		}
User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);

9、解决超卖问题

2、数据库上的优化,增加user_id、goods_id的索引,每次卖的时候读数据库检查缓存是否存在。解决用户秒杀同一商品。
3、在sql语句中增加库存数量的判断,防止库存数量变成负数。
4、将商品存入redis,加快处理速度,并用decrement()函数保证原子性的减库存。

*5、实现乐观锁,给商品信息表增加一个version字段,为每一条数据加上版本。每次更新的时候version+1,并且更新时候带上版本号,当提交前版本号等于更新前版本号,说明此时没有被其他线程影响到,正常更新,如果冲突了则不会进行提交更新。当库存是足够的情况下发生乐观锁冲突就进行一定次数的重试




 boolean result = seckillGoodsServices.update(new UpdateWrapper<TSeckillGoods>().set
                ("stock_count", goods.getStockCount()).eq("id", goods.getGoodsId()).gt
                ("stock_count", 0));


redisTemplate.opsForValue().set("order" + user.getId() + ":" + goods.getGoodsId(),seckillOrder);

TSeckillOrder seckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodId);


//实现超卖的时候减库存,原子操作
redisTemplate.opsForValue().decrement("seckillGodos:" + goodsId);


3、redis内存标记,实现redis访问速度。

4、高并发环境下的秒杀场景。使用消息队列进行异步下单,使用队列进行缓冲,解决并发延迟问题。

10. rabbitMQ

生产者 + 交换机 + 队列 + 消费者
在这里插入图片描述
交换机的四种模式:
direcet:type = direct 准确路由
在这里插入图片描述

topic:正则通配符录取
headers(效率低,一般不用):
fanout:广播模式,每个队列都能收到消息

本地标记 + redis预处理 + RabbitMQ异步下单 + 客户端轮询
描述:通过三级缓冲保护:
1、本地标记 2、redis预处理 3、RabbitMQ异步下单,最后才会访问数据库,这样做是为了最大力度减少对数据库的访问。

实现:
1、 初始化就阶段将秒杀商品的数量存入redis中
重写InitializingBean中的afterPropertiesSet()方法
2、 秒杀线程访问服务器,先访问Redis中是否存在库存,若没库存,则直接返回。
valueOprerationals.decrement(Key);//减库存
3、 使用rabbitmq异步生成订单。(前端轮询查数据)
失败 -1
查询中 0
秒杀成功 orderId
4、服务器从通过rabbitmq拿到请求,若mysql减库存成功则生成订单,若减库存失败,则对redis进行更新。(使用注解@Transactional标记)

5000个线程,每个线程访问10次,QPS:3600+

11、秒杀接口隐藏

前端通过path参数获取秒杀接口

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值