商城秒杀项目开发目录
基于慕课网实战上完成商城的秒杀项目,视频链接
项目框架搭建
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参数获取秒杀接口