页面详情页面
用户在抢购页面会不断刷新,不能每次都到数据读取信息。将页面信息保存进缓存中实现快速读的功能。缓存优化,无限前置化,异步多活。了解不同的优化方案。JVM的ehcache,静态页面,CDN,Nginx。用户的电脑。动态资源最多前置到网关,因为需要更新。
缓存前置化:
缓存有2个重要的运用方式:预读取和延迟写。
预读取就是预先读取将要载入的数据,也可以称作“缓存预热”,它是在系统中先将硬盘中的一部分数据加载到内存中,然后再对外提供服务。
通过缓存机制来加速“写”的过程就可以称作“延迟写”,它是预先将需要写入到磁盘或者数据库的数据,暂时写入到内存,然后就返回成功,再定时将内存中的数据批量写入到磁盘。
哪里可以加缓存?
热点数据:被高频访问,如几十次/秒以上
静态数据:很少变化,读远大于写,如几天变更一次

每个设立点可以挡掉一些流量,最终形成一个漏斗状的拦截效果,以此保护最后面的系统以及最终的数据库。

下面简要描述一下每个运用场景以及需要注意的点。
浏览器缓存
这是离用户最近的可以作为缓存的地方,而且借助的是用户的“资源”(缓存的数据在用户的终端设备上),性价比可谓最好,让用户帮你分担压力。

当你打开浏览器的开发者工具,看到from cache或者from memory cache、from disk cache的时候,就意味着这些数据已经被缓存在了用户的终端设备上了,没网的时候也能访问到一部分内容就是这个原因。
这个过程是浏览器替我们完成的,一般用于缓存图片、js与css这些资源,我们可以通过Http消息头中的Cache-Control来控制它,具体细节这里就不展开了。此外,js里的全局变量、cookie等运用也属于该范畴。
浏览器缓存是在于用户侧的缓存点,所以我们对它的掌控力会比较差,在没有发起新请求的情况下,你无法主动去更新数据。
CDN缓存
提供CDN服务的服务商,在全国甚至是全球部署着大量的服务器节点(可以叫做“边缘服务器”)。
那么将数据分发到这些遍布各地服务器上作为缓存,让用户访问就近的服务器上的缓存数据,就可以起到压力分摊和加速效果。这在toC类型的系统上运用,效果格外显著。
但是需要注意的是,由于节点众多,更新缓存数据比较缓慢,一般至少是分钟级别,所以一般仅适用于不经常变动的静态数据。
题外话:解决方式也是有的,就是在url后面带个自增数或者唯一标示,如?v=1001。因为不同的url会被视作“新”的数据和文件,被重新create出来。
网关(代理)缓存
到这里做缓存就是在你自己的地盘了。很多时候我们会在源站前面架一层网关(或者说反向代理、正向代理),为的是做一些安全机制或者作为统一分流策略的入口。

同时这里也是做缓存的一个好场所,毕竟网关是“业务无关性”的,它能够拦下来请求,对背后的源站也有很大的受益,减少了大量的CPU运算。
常用的网关(代理)缓存有Varnish、Squid与Ngnix。一般情况下,简单的缓存运用场景,用Nginx即可,因为大部分时候我们会用它来做负载均衡,能少引入一个技术就少一份复杂度。如果是大量的小文件可以使用Varnish,而Squid则相对大而全,运用成本也更高一些。
进程内缓存
可能我们大多数程序员第一次刻意使用缓存的场景就是这个时候。
一个请求能走到这里说明它是“业务相关”的,需要经过业务逻辑的运算。
也正因为如此,从这里开始对缓存的引入成本比前面3种大大增加,因为对缓存与数据库之间的“数据一致性”要求更高了。
进程外缓存
这个大家也熟悉,就是Redis与Memcached之类,甚至也可以自己单独写一个程序来专门存放缓存数据,供其它程序远程调用。
这里先多说几句关于Redis和Memcached该怎么选择的思路。
对资源(cpu、内存等)利用率格外重视的话可以使用Memcached,但程序在使用的时候需要容忍可能发生的数据丢失,因为是纯内存的机制。如果无法容忍这点,并且对资源利用率也比较豪放的话可以使用Redis。而且Redis的数据库结构更多,Memcached只有key-value,更像是一个NoSQL存储。
数据库缓存
数据库本身是自带缓存模块的,否则也不会叫它内存杀手,基本上你给多少内存就能吃多少。数据库缓存是数据库的内部机制,一般都会给出设置缓存空间大小的配置来让你进行干预。
最后,其实磁盘本身也有缓存。所以你会发现,为了让数据能够平稳地写到物理磁盘中真的是一波三折,不知道什么时候可以有“快”到不需要程序来考虑缓存的磁盘出现来拯救我们程序员呢。
课堂正题
怎么获取到url中的参数值:
拆 ?后面的字符串,再根据 & 对字符串拆分, 再以 = 拆成变量和值。
// 获取地址栏参数function getQueryString(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");var r = window.location.search.substr(1).match(reg);if (r != null) return unescape(r[2]);return null;}
秒杀商品详情信息展示:

controller:
findById方法和query中的操作大部分是一样的,区别就是一个查的是集合,一个查的是单个而已。将公共的部分抽取成一个方法。查单个就是list中只装一个对象即可;
@Servicepublic class SeckillGoodServiceImpl implements ISeckillGoodService {@Autowiredprivate SeckillGoodMapper seckillGoodMapper;@Autowiredprivate GoodFeignApi goodFeignApi; // 涉及RPC 检查启动类有无注解:@EnableFeignClients//查秒杀商品列表@Overridepublic List<SeckillGoodVO> query() {// 秒杀商品列表的查询// 1.查询商品秒杀的列表List<SeckillGood> seckillGoodList = seckillGoodMapper.listAll();List<SeckillGoodVO> seckillGoodVOList = getVo(seckillGoodList);return seckillGoodVOList;}//通过 id 查秒杀商品@Overridepublic SeckillGoodVO findById(Long id) {SeckillGood seckillGood = seckillGoodMapper.selectByPrimaryKey(id);// List<SeckillGood> seckillGoodList = new ArrayList<>(1);// seckillGoodList.add(seckillGood);List<SeckillGood> seckillGoodList = Collections.singletonList(seckillGood);List<SeckillGoodVO> seckillGoodVOList = getVo(seckillGoodList);return seckillGoodVOList.size() > 0 ? seckillGoodVOList.get(0) : null;}// 抽取出来的查询秒杀商品的方法,对 query 和 findById 复用private List<SeckillGoodVO> getVo(List<SeckillGood> seckillGoodList) {// 2.根据商品秒杀的列表,得到商品 id 的列表, ids 应该是Set集合,因为不同场秒杀关联的商品可能相同Set<Long> goodIds = new HashSet<>(seckillGoodList.size());for (SeckillGood seckillGood : seckillGoodList) {goodIds.add(seckillGood.getId());}// 3.通过 feign 远程调用商品服务,查询列表// TODO 商品服务的远程调用,通过 feign 暴露的接口Result<List<Good>> result = goodFeignApi.getGoodListByIds(goodIds);// 商品服务可能返回三种结果:1. 正常返回; 2. 降级的null;3. code != 200 返回resultif (result.hasError()) { // result == null || result.getCode() != 200 的情况是有错误// 返回 null 或者抛出异常给调用者响应的提示throw new BusinessException(SeckillServerCodeMsg.DEFAULT_ERROR);}//正常调用返回的数据List<Good> goodList = result.getData();// 通过遍历的方式来给属性赋值,效率低。采用HashMap做缓存的方式来操作HashMap<Long, Good> tempCache = new HashMap<>(goodList.size());// 把查到的列表先丢到map中for (Good good : goodList) {tempCache.put(good.getId(), good);}// 4.遍历商品列表,将列表中每项的属性,复制到 VO 中,得到 vo 列表List<SeckillGoodVO> seckillGoodVOList = new ArrayList<>(seckillGoodList.size());for (SeckillGood seckillGood : seckillGoodList) {Good good = tempCache.get(seckillGood.getGoodId());// 拿到map中的值SeckillGoodVO vo = new SeckillGoodVO(); // 创建vo对象,复制两者属性// ------ 注意属性复制的顺序:vo的id应该和秒杀商品的id相同 -------// * 复制商品的属性if (good != null)//有值才复制属性:BeanUtilsBeanUtils.copyProperties(good, vo);// * 复制秒杀商品的属性BeanUtils.copyProperties(seckillGood, vo);seckillGoodVOList.add(vo); // 添加进list中}return seckillGoodVOList;}}
判断用户是否登录:
商品详情的前提是需要用户登陆之后才可以有展示效果,用户没有登录就提示用户进行登录。
prop

member-server的controller:
使用了前台传过来的token去Redis中查询了登陆过的用户信息。
更好的方式:自定义参数解析器

响应回来的信息不能把密码等敏感信息返回:不让他序列化,就不会再网络中进行传输了。

定义参数解析器
用自定义参数解析器,来优化验证用户是否登录的过程:
controller可以变得这样简单:
// 查询当前登录用户信息@RequestMapping("/current")public Result<User> current(@UserParam User user) {return Result.success(user);}
自定义的标识注解:
目的:注册和登录的时候的参数user不应该被我们定义的解析器给解析了。两者情况场景不一样,用注解标记区分开。
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface UserParam {}
自定义的参数解析器:
/*** 用户参数解析器*/public class UserArgumentResolver implements HandlerMethodArgumentResolver {@Autowiredprivate StringRedisTemplate redisTemplate;// 参数解析的条件@Overridepublic boolean supportsParameter(MethodParameter methodParameter) {return User.class.equals(methodParameter.getParameterType()) &&methodParameter.hasParameterAnnotation(UserParam.class);}// 解析逻辑@Overridepublic Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {//拿到请求中的tokenHttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); // 指定类型转成 HttpServletRequest 类型的请求if (request == null) {return null;}// 拿到Redis中的keyString token = CookieUtils.getCookieValue(request, CookieUtils.USERTOKEN_NAME);if (StringUtils.isEmpty(token)) {return null;}// 通过 key 查询数据String userJson = redisTemplate.opsForValue().get(RedisKeys.USER_LOGIN_TOKEN.join(token));if (StringUtils.isEmpty(userJson)) {return null;}return JSON.parseObject(userJson, User.class);}}
需要注册注解解析器:才会生效
创建配置类的方式:@Configration
implements WebMVCConfigurer
重新addXxx方法;
/*** 配置类:参数解析器和跨域等各种配置不应该放在启动类里,启动类中一个main方法就可以了* 一切配置放这里*/@Configuration // 标识这是一个配置类public class WebConfig implements WebMvcConfigurer {// 让Spring创建参数解析器的实例@Beanpublic UserArgumentResolver userArgumentResolver() {return new UserArgumentResolver();}// 将参数解析器的实例添加注册@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(userArgumentResolver());}}
前端:
详情页面的秒杀按钮,应该有倒计时,并且时间未到不能点击;
代码拷贝;
秒杀流程:

前端代码:

创建订单对象实体类:
拷贝过来,放在seckill-api中;
该服务中使用到自定义的参数解析器,需要一个配置类,注意:配置类不可以复用,每个server应该是单独的WebConfig类。
这样做虽然提升了性能,但是同时也增加了服务之间的耦合:
@Configurationpublic class WebConfig implements WebMvcConfigurer {@Beanpublic UserArgumentResolver userArgumentResolver(){return new UserArgumentResolver();}@Overridepublic void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(userArgumentResolver());}}
还要注意:这里用户是从Redis中获取到的,我们使用到redis-server服务,所以我们要加上配置将它引入进来:

controller:根据订单流程来完成

创建基础订单的时候:orderNo做主键

秒杀服务的服务端做了集群之后,每个服务注册到注册中心,用户通过负载均衡来访问到服务器。如果集群很多,那么压力就到了数据库这里,因为所有的请求都直接访问到额是数据库。数据量随着生产期的增加会不断增加,所以需要对表进行分库分表操作,不同的数据库不同的表,此时再用id做注解就不行了,id都是从1开始的。所以这里选择使用orderNo做主键。企业开发中有使用雪花算法(SnowFlake)来做主键进行分库分表。
@RestController@RequestMapping("/api/orders")public class OrderInfoController {@Autowiredprivate ISeckillGoodService seckillGoodService;@Autowiredprivate ISeckillOrderService seckillOrderService;@Autowiredprivate IOrderInfoService orderInfoService;@RequestMapping("/doSeckill")public Result<String> doSeckill(Long seckillId, @UserParam User user) {// 校验逻辑if (user == null || seckillId == null) {throw new BusinessException(SeckillServerCodeMsg.OPS_ERROR);}// 1. 获取秒杀商品对象,判断秒杀活动是否存在SeckillGoodVO seckillGoodVO = seckillGoodService.findById(seckillId);if (seckillGoodVO == null) {throw new BusinessException(SeckillServerCodeMsg.OPS_ERROR);}// 2. 判断当前时间是否在秒杀活动的时间内Date now = new Date(); // 当前时间// 当前小于开始时间,活动未开始if (now.compareTo(seckillGoodVO.getStartDate()) < 0) {throw new BusinessException(SeckillServerCodeMsg.NOT_START_ERROR);}// 当前大于等于结束时间,活动已结束if (now.compareTo(seckillGoodVO.getEndDate()) >= 0) {throw new BusinessException(SeckillServerCodeMsg.ALREADY_OVER_ERROR);}// 3. 判断当前用户是不是重复下单了,不允许重复下单SeckillOrder seckillOrder = seckillOrderService.findBySeckillIdAndUserId(seckillId, user.getId());if (seckillOrder != null) {throw new BusinessException(SeckillServerCodeMsg.ALREADY_AGEIN_ERROR);}// 4. 判断秒杀商品的库存是否足够if (seckillGoodVO.getStockCount() <= 0){throw new BusinessException(SeckillServerCodeMsg.STOCKCOUNT_OVER_ERROR);}// 5. 创建秒杀订单:// 包括 1.减库存 2.创建普通订单(秒杀订单依赖普通订单) 3.创建秒杀订单String orderNo = orderInfoService.doSeckill(seckillId, user.getId());return Result.success(orderNo);}}
doSeckill做秒杀操作的方法:
注意:@Transactional(rollbackFor = Exception.class)
DML 操作,要保证原子性等,要加上事务,显示的指定Exception
public class OrderInfoServiceImpl implements IOrderInfoService {@Autowiredprivate ISeckillGoodService seckillGoodService;@Autowiredprivate ISeckillOrderService seckillOrderService;@Autowiredprivate OrderInfoMapper orderInfoMapper;// DML 操作,要保证原子性等,要加上事务,显示的指定Exception@Transactional(rollbackFor = Exception.class)public String doSeckill(Long seckillId, Long userId) {// 做秒杀操作,包括// 1.减库存 t_seckill_goodsseckillGoodService.decrStockCount(seckillId);// 2.创建普通订单(秒杀订单依赖普通订单) t_order_infoString orderNo = this.creatOrder(seckillId, userId);// 3.创建秒杀订单 t_seckill_orderseckillOrderService.creatSeckillOrder(seckillId, userId, orderNo);return orderNo;}// 2.创建普通订单private String creatOrder(Long seckillId, Long userId) {SeckillGoodVO vo = seckillGoodService.findById(seckillId);OrderInfo orderInfo = new OrderInfo();// 分布式唯一主键String orderNo = "";long id = IdGenerateUtil.get().nextId();orderNo = orderNo + id; // 主键一般用 String// generatAll 插件快速生成orderInfo.setOrderNo(orderNo); // TODOorderInfo.setGoodCount(1); // 商品数量orderInfo.setUserId(userId);orderInfo.setGoodId(vo.getGoodId());orderInfo.setGoodName(vo.getGoodName());orderInfo.setGoodImg(vo.getGoodImg());orderInfo.setGoodCount(vo.getStockCount());orderInfo.setGoodPrice(vo.getGoodPrice());orderInfo.setSeckillPrice(vo.getSeckillPrice());orderInfo.setCreateDate(vo.getEndDate());// 保存订单对象orderInfoMapper.insert(orderInfo);return orderNo;}}
订单详情:
前端代码:

controller:
存在一个问题,通过id就可以查询订单信息了(这样订单信息不安全),查询的时候必须是当前用户查询的。

impl:

查询条件:
小伙砸,欢迎再看分享给其他小伙伴!共同进步!




