配置文件org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-web mysql mysql-connector-java runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.baomidou mybatis-plus-boot-starter 3.3.1.tmp
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 #30分钟
最大连接数
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
@RequestMapping("/hello")
public String hello(Model model){
model.addAttribute("name","mgy");
return "hello";
}
}
- 前端页面(在templates端口下)
hello.html
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加密,一次输入密码时加密(前端传给后端),存入数据库时再加密
commons-codec commons-codec org.apache.commons commons-lang3 3.6
@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;
}
}
创建逆向工程的模块
规定返回类型
写一个RespBean用于统一返回类型 登录页面
一个方法跳转登录页面登录页面将信息提交到doLogin
RestController=Controller+ResponseBody
手机号码格式校验
@Pattern(regexp = "[1]([3-9])[0-9]{9}$",message = "手机号码格式错误")
@Valid异常捕获
@RestControllerAdvice@ExceptionHandler(Exception.class) 登录信息传递
//生成cookie
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){
//判断ticket是不是空,如果是空,代表没有用户信息,需要去登录
if(StringUtils.isEmpty(ticket)){
return "login";
}
User user = (User)session.getAttribute(ticket);
if(user==null){
return "login";
}
//如果user存在,说明已经登录,将用户对象传到前端页面
model.addAttribute("user",user);
return "goodsList";
}
分布式session
org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 org.springframework.session spring-session-data-redis
使用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;
}
//生成cookie
String ticket = UUIDUtil.uuid();
redisTemplate.opsForValue().set("user:"+ticket,user);
// request.getSession().setAttribute(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("
@Override
public Long getResult(User user, Long goodsId) {
QueryWrapper wrapper = new QueryWrapper().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锁
一个线程去操作redis就会占位,别的线程就无法操作,这个线程执行完之后删除锁
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testLock01() {
ValueOperations valueOperations = redisTemplate.opsForValue();
//随便设置一个key,每个线程都有去尝试存入这个key,只有这个key不存在的情况下才能存入,返回是否存入成功。
Boolean isLock = valueOperations.setIfAbsent("k1", "v1");
if(isLock){
valueOperations.set("name","mgy");
String name = (String) valueOperations.get("name");
System.out.println(name);
//一个线程结束后将k1删除,下一个线程才能进来
redisTemplate.delete("k1");
}
else{
System.out.println("有线程在使用,请稍后");
}
}
使用lua脚本能先获取锁,然后判断锁的值是否一致,然后再进行删除(保证每次只删自己的锁,不删别人的锁)
这三个操作在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");
//将每个用户随机生成的接口地址存在redis,之后进行校验
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){
//如果还没有这个,就设置一个,此时值为1
valueOperations.set(uri+":"+user.getId(),1,5,TimeUnit.SECONDS);
}else if(count<5){
//如果值小于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
秒杀主流方案分析
需要注意的问题
高并发,刷接口等黑客请求,超出负载高并发时导致的超卖该负载情况下下单成功率的保障
网关限流
黑名单,放在内存里多次请求,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
秒杀主流方案分析
需要注意的问题
高并发,刷接口等黑客请求,超出负载高并发时导致的超卖该负载情况下下单成功率的保障
网关限流
黑名单,放在内存里多次请求,redis缓存重复没有预约(没有令牌)预约可以提前发放部分令牌redission加分布式锁,redis集群



