秒杀P5-商品列表

163 阅读3分钟

1、状态管理

很多功能界面都需要验证登录状态(需要已登录),登录时把状态(存数据表示状态就可以,不用非要status状态码)记住

不适合AOP:面向方法名/对象角度过滤。

拦截器:面向url过滤

拦截器+session进行状态管理

去看登录方法

UserController

登录成功以后(要记住状态),获取到用户完整信息,存到session中。

后续要验证,直接在session中搜索user

image.png

退出以后要清除状态,调用session.invalidate()方法,让session自己销毁

image.png

登录后要在页面显示用户信息,显示登陆状态,提供方法从session获取用户信息,返回给页面处理

image.png

controller/Interceptor/LoginChecklnterceptor

登录检查拦截器,很多页面统一验证登录状态

  • @Component受容器管理
  • 实现HandlerInterceptor接口【HandlerInterceptor接口的三个方法,对应拦截器拦截的时机】
  • 重写prehandle方法,返回布尔类型。参数的请求和响应,despachservelitxxx自动传入
  • 从请求中获取session
  • 从session中尝试获取用户。
  • 若user为空没登陆,Interceptor返回false。在Interceptor1处拦截,不会执行controller

输出json代码解读未看

@Component
public class LoginCheckInterceptor implements HandlerInterceptor, ErrorCode {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(); //请求中获取session
        User user = (User) session.getAttribute("loginUser");
        if (user == null) {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            PrintWriter writer = response.getWriter();
            Map<Object, Object> data = new HashMap<>();
            data.put("code", USER_NOT_LOGIN);
            data.put("message", "用户未登录!");
            ResponseModel model = new ResponseModel(ResponseModel.STATUS_FAILURE, data);
            writer.write(JSONObject.toJSONString(model));
            return false;
        }
        return true;
    }

}

image.png

configuration/WebMvcConfiguration

具体对哪些请求拦截还不明朗,需要注册声明。所以自己写一个WebMvcConfiguration配置类。

  • @Configuration配置类注解
  • 实现WebMvcConfigurer接口
  • 重写WebMvcConfigurer接口的addInterceptors(InterceptorRegistry registry)注册拦截器的(注册机)方法
  • WebMvcConfigurer接口底层实例化了registry对象,我们只要把我们实例化的拦截器注入到这个bean下,把拦截器组件传给registry。
  • 声明拦截器对哪些路径有效【可以有多个路径拦截】
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/order/create");
    }

}

拦截器小结:

  1. 写一个LoginCheckInterceptor 实现 HandlerInterceptor接口。写方法,在合适时机作拦截处理
  2. 注册声明拦截哪些

2、跨域

跨域——在一个域名(页面)中,访问另一个域名/服务器的资源

一个路径的xxx.thml页面,调用了xxx.js,xxx.js中的ajax代码,访问了另一个路径

image.png

image.png

用Nginx代理

实际上没有跨域,要求前后端地址要规范,格式受限

image.png

标签

js标签,欺骗性,局限性。只能get请求

image.png

HTTP协议的跨域资源共享(CORS)

后端同意访问就行,加一段注解

@CrossOrigin(origins = "${nowcoder.web.path}", allowedHeaders = "*", allowCredentials = "true")

@CrossOrigin注解——声明服务端允许跨域

origins = "${nowcoder.web.path}"——哪些路径可以跨域。

nowcoder.web.path——application中配置的参数,值就是127.0.0.1:5500,表示这个端口可以跨域访问我

allowedHeaders = "*" ——跨域时可以读取header里的内容

allowCredentials = "true"——可以存/使用cookie

image.png 前端也加内容,表示允许带cookies

image.png

cookie&session

因为业务连续性,需要保存客户端状态,让服务端能够记住。

cookie

保存在浏览器,存json字符串,内存很小,不安全

image.png

session 保存在服务器,可存object,安全性强,但会加重服务器压力,默认半小时过期。依赖于cookie

  1. 首次访问首页,服务器生成session,把用户信息存【到session对象中】起来。此处为空的session对象。
  2. 返回【存的是sessionID】的cookie给浏览器保存
  3. 登录,浏览器带着cookie来,由sessionID获取到对应session对象,首次登录为空session对象,存入用户信息
  4. 再次访问/访问其他子页面,浏览器带着cookie来,由sessionID获取到对应session对象,读取用户信息

image.png

分布式场景

多个服务器

存到redis中的session设置过期时间

image.png

token

多客户端+分布式 场景

image.png

单点登录SSO

登陆一次可以访问多个网站

根域名相同

设置cookie的生效范围

image.png

域名不同

基于中间服务器颁发全局token验证

并且每个域名/子服务器/关联服务器,颁发局部token。

服务器间的验证,通过web.service

redirect(地址) 重定向,浏览器自动的行为

全局token失效时,会通知局部token注销

用户信息存到local.story/session.story比cookie安全

中间服务器登录以后:

  1. 会给客户端返回token(通过cookie实现?)
  2. 会要求客户端重定向到原来访问的地址
  3. 会给客户端创建cookie保存用户信息

image.png

每个服务器/网址背后都有对应的redis存各自的token

image.png

商品列表

用户表, 商品表:id,标题,价格,销量,主图路径,描述 商品库存表:id,库存id==映射商品id,库存值 活动表:id,name(活动名),开始介绍时间,商品id,活动价格

商品和库存分表,因为如果不分表(库存字段在商品表中),下单减库存要锁整行,后期高并发场景,要刷信息,你在读,不行。

锁库存表的行,不影响读商品表。

entity/Item、ItemStock:实体类。

  • 写属性(字段),加注解给字段限制条件。
  • 重写tostring方法

dao/ItemMapper、ItemStockMapper:dao层,接口文件

  • 单纯定义(生成)增删改查方法【方法体内容在mapper.xml的sql文件中】
  • 写增加销量方法,查询秒杀活动的商品

resources/mappers/ItemMapper.xml、ItemStockMapper.xml:mapper层,sql文件

<!-- 查询正在进行秒杀活动的商品 -->
<select id="selectOnPromotion" resultMap="BaseResultMap">
  select id, title, price, sales, image_url, description
  from item
  where exists (
    select 1 from promotion
    where item_id = item.id
    and start_time&lt;=CURRENT_TIMESTAMP and end_time&gt;=CURRENT_TIMESTAMP
  )
</select>

&lt;是小于<
&gt;是大于>

service/ItemService:业务层。接口

只写要实现的业务/功能的方法。

查询所有参与秒杀活动的商品的方法,findItemsOnPromotion()

public interface ItemService {

    List<Item> findItemsOnPromotion();

    Item findItemById(int id);

    boolean decreaseStock(int itemId, int amount);

    void increaseSales(int itemId, int amount);

}

service/impl/ItemServiceImpl:业务层的实现类

写方法体内容

findItemsOnPromotion()方法,查询所有参与秒杀活动的商品,调用dao层的itemMapper的方法

但是不只要查items表,还要获取item_stock和promotion表的内容,怎么处理?

  • 关联查询
  • 缓存优化
  • Java8流式语法

流式循环,循环遍历item,每次循环,由item查询库存+活动,填到item中。

  • 把list转换成流,对流做处理,每次处理的是一个item的粒度。

  • 获取itemId去查询库存+活动,存到item(用set方法)

  • 最后把流转换成list对象

item实体类中加入了,库存、活动两引用类型的字段

public List<Item> findItemsOnPromotion() {
    List<Item> items = itemMapper.selectOnPromotion();
    return items.stream().map(item -> {
        // 查库存
        ItemStock stock = itemStockMapper.selectByItemId(item.getId());
        item.setItemStock(stock);
        // 查活动
        Promotion promotion = promotionMapper.selectByItemId(item.getId());
        if (promotion != null && promotion.getStatus() == 0) {
            item.setPromotion(promotion);
        }
        return item;
    }).collect(Collectors.toList());
}

itemController

商品详情,列表,P4的2h10min左右?

索引

image.png

用法

image.png

最左匹配原则

image.png

多个索引,是分层排序。

image.png

小结

  • entity:实体类,如user
  • dao:数据层,实体类相应的接口,接口内只声明方法,userMapper
  • mapper(.xml):mapper层,sql(配置)文件,写接口方法的sql语句,userMapper.xml
  • service:业务层,接口只声明业务方法,UserService
  • service/impl:业务层接口的实现类,写接口方法的具体内容(方法体),UserServiceImpl

注:service/impl实现类的方法里,可调用dao层的接口