From 7c915c69131625eec93eff9ead9a6fd37a87ab54 Mon Sep 17 00:00:00 2001 From: wardseptember Date: Sun, 25 Oct 2020 14:23:31 +0800 Subject: [PATCH] =?UTF-8?q?update:=20=E6=9B=B4=E6=96=B0=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../spring/spring\345\237\272\347\241\200.md" | 2 + ...14\351\253\230\345\271\266\345\217\221.md" | 20 + ...46\344\271\240\347\254\224\350\256\260.md" | 480 +++++++++++++++++- 4 files changed, 488 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index df23eeb..ca54c29 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ 关注我获得最新笔记、更多资源 -

+

+ ## GPL v2.0 [LICENSE](https://github.com/wardseptember/notes/blob/master/LICENSE) diff --git "a/docs/spring/spring\345\237\272\347\241\200.md" "b/docs/spring/spring\345\237\272\347\241\200.md" index 4955988..471ac40 100644 --- "a/docs/spring/spring\345\237\272\347\241\200.md" +++ "b/docs/spring/spring\345\237\272\347\241\200.md" @@ -132,6 +132,8 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring M 7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 8. 把 `View` 返回给请求者(浏览器) +**SpringMVC通过DispatcherServlet拦截所有的请求, 并通过HandlerMapping与指定的请求找出匹配的handler, handler实际是HandlerMethod对象。 再通过与handler适配的HandlerAdapter执行目标方法, 执行完目标方法后会返回ModelAndView对象, 最后通过ViewResolver解析ModelAndView的View视图。** + 更详细教程: * https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/framework/spring/SpringMVC-Principle.md diff --git "a/docs/\345\244\232\347\272\277\347\250\213\345\222\214\351\253\230\345\271\266\345\217\221.md" "b/docs/\345\244\232\347\272\277\347\250\213\345\222\214\351\253\230\345\271\266\345\217\221.md" index 3ade29a..1ebc107 100644 --- "a/docs/\345\244\232\347\272\277\347\250\213\345\222\214\351\253\230\345\271\266\345\217\221.md" +++ "b/docs/\345\244\232\347\272\277\347\250\213\345\222\214\351\253\230\345\271\266\345\217\221.md" @@ -411,7 +411,10 @@ volatile作用: Volatile并不能保证多个线程共同修改running变量说带来的不一致性问题,也就是说volatile不能替代synchronized。 + + ```java + import java.util.ArrayList; import java.util.List; @@ -447,8 +450,11 @@ volatile作用: System.out.println(t.count); } } + ``` + + count++被编译成字节码,会分成三个指令,第一个从主内存拿到原始count,第二个在工作线程中执行+1操作,第三把累加后的值写回主内存。 上面的程序输出count的结果并不是10000,这是因为volatile不能保证原子性导致的脏读。对m()加synchronized关键字可以保证原子性,count最后结果一定是10000。 @@ -459,7 +465,10 @@ volatile作用: 下面代码几秒之内并不会输出"m end!"。 + + ```java + import java.util.concurrent.TimeUnit; public class T02_VolatileReference1 { @@ -490,8 +499,11 @@ volatile作用: T.running = false; } } + ``` + + #### Volatile实现原理 * 可见性实现原理 @@ -1068,7 +1080,10 @@ reentrantlock可用于替代synchronized,它比synchronized的功能更强大 3. 使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应。 + + ```java + import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -1118,8 +1133,11 @@ reentrantlock可用于替代synchronized,它比synchronized的功能更强大 } } + ``` + + 4. ReentrantLock还可以指定为公平锁 ```java @@ -2092,6 +2110,8 @@ ThreadLocal使用完了,手动remove掉。 插入效率低一些,查询时效率高。 +教程见[ConcurrentHashMap详解(基于1.7和1.8)](https://wardseptember.gitee.io/mynotes/#/docs/ConcurrentHashMap详解(基于1.7和1.8)) + ### ConcurrentSkipListMap 是一个有序的并发Map。 diff --git "a/docs/\350\260\267\347\262\222\345\225\206\345\237\216\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/docs/\350\260\267\347\262\222\345\225\206\345\237\216\345\255\246\344\271\240\347\254\224\350\256\260.md" index 39763a0..295452a 100644 --- "a/docs/\350\260\267\347\262\222\345\225\206\345\237\216\345\255\246\344\271\240\347\254\224\350\256\260.md" +++ "b/docs/\350\260\267\347\262\222\345\225\206\345\237\216\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -201,15 +201,25 @@ POST _reindex # 购物车 * 离线购物车 -* 在线购物车 -使用ThreadLocal共享数据,同一个用户请求,都用的同一个线程。 +Cookie 存一个user-key,标识用户身份 + +第一次访问,没有user-key,创建一个user-key,存到cookie中;并把用户信息存到threadlocal中 + +商品信息存到redis中, + +```java +String cartKey = !ObjectUtils.isEmpty(userInfoTO.getUserId()) ? CART_PREFIX + userInfoTO.getUserId() : CART_PREFIX + userInfoTO.getUserKey(); + +``` + +* 在线购物车 -wardseptember/mynotes +使用ThreadLocal共享数据,同一个用户请求,都用的同一个线程。 -GITEE_RSA_PRIVATE_KEY +将临时购物车数据合并到在线购物车中 @@ -242,7 +252,7 @@ public class GulimallFeignConfig { ## Feign异步远程调用丢失请求头 -``` +```java RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture getAddressedTask = CompletableFuture.runAsync(() -> { @@ -498,7 +508,7 @@ Google 的 Guava 包中的 RateLimiter 类就是令牌桶算法的解决方案 - HPS(Hits Per Second): 每秒点击次数 - TPS(Transaction Per Second) -- QPS(Query Per Scond) +- QPS(Query Per Second) - 最大响应时间 - 最少响应时间 - 90%响应时间 @@ -549,11 +559,31 @@ public class MallWebConfig implements WebMvcConfigurer { ## RabbitMQ +消息:消息头 消息体 路由键 + +消息的生产者 + +交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。 + +broker是消息代理 + +Exchange类型 + +* direct是直接点对点的交互接, +* fanout是广播交换机,无论路由键是什么,全部queue都能收到 +* topic是主题交换机,可以匹配主题的 +* header也是点对点,效率低,不使用 + +

+ +### 特点 + * 一个客户端只会建立一个长链接,长连接里面可以有很多channel * 消息发布者是将消息发给exchange,exchange将消息分发给queue,channel对接上queue就可以接受消息了 * Broker里面可以有多个虚拟主机,各个虚拟主机之间互不影响。 * Exchange和queue之间靠binding连接着,binding决定交换机的消息应该发送到那个队列 * exchange也可以跟exchange绑定 +* 消息中的路由键如果和Binding中的binding key一致,交换机就将消息发送到对应的队列中。 ``` docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management @@ -561,21 +591,13 @@ docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25 docker update rabbitmq --restart=always ``` -Exchange类型 - -* direct是直接点对点的交互接, - -* fanout是广播交换机,无论路由键是什么,全部queue都能收到 - -* topic是主题交换机,可以匹配主题的 - -* header也是点对点,效率低,不使用 - 一些点 * @RabbitLister 可以标注在类和方法上,监听哪些队列即可 * @RabbitHandler: 标在方法上,重载区分不同消息 +

+ ### 应用 - 异步处理 @@ -596,6 +618,43 @@ queue到consumer会确认 ![](https://gitee.com/wardseptember/images/raw/master/imgs/20200917212340.png) +## 提交订单 + +接口幂等性:用户对同一操作发起的一次请求或者多次请求的结果都是一致的。 + +问题: + +* 用户多次点击按钮提交 +* 用户页面回退再次提交 +* 微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制。 + +解决: + +> 下单 去创建订单 验证令牌 核算价格 锁定库存 + +### 防止重复提交 + +来到订单页的时候,页面加入防重令牌,并且redis中存入防重令牌,提交订单时获取令牌、比较、删令牌原子操作 + +```java + SubmitOrderResponseVO responseVO = new SubmitOrderResponseVO(); + MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get(); + responseVO.setCode(0); + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + String orderToken = submitVO.getOrderToken(); + // 原子性操作验证和删除令牌 + Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), + Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken); +``` + +### 核算价格 + +把提交的总价格和购物车中的所有项的价格对比 + +### 锁定库存 + +可靠消息加最终一致性 + ## Feign远程调用丢失请求头问题 `Feign`在远程调用之前要构造请求,此时会丢失请求头`headers`,`request`中包含许多拦截器。 @@ -685,3 +744,392 @@ CAP定理 - 上线更多的消费者,进行正常消费。 - 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理。 + + + + +# 2020.10.21 + +* 后台管理系统 +* 购物车、订单、结算、库存、秒杀 +* 分布式事务、分布式锁 +* 高并发:线程池、异步编排 +* 商品服务、优惠服务、用户服务、仓储服务、秒杀服务、订单服务、检索服务、购物车服务 + + + +客户端发送请求给nginx,nginx把请求转交给api网关,网关把请求动态路由到指定服务, + +nginx代理给网关的时候,会丢失请求的host信息。 + +# 缓存 + +适合放入缓存: + +* 即时性、数据一致性要求不高的 +* 访问量大且更新频率不高的数据 + +本地缓存map,会存在数据不一致情况 + +分布式缓存 + +Data-redis 使用lettuce,lettuce的bug,使用jedis代替lettuce。 + +## 分布式锁 + +### 第一阶段 + +set key value NX + +nx只有key不存在的时候才会设置key的值 + +

+ +问题: + +* 没有执行删除锁代码,产生死锁 + +解决: + +* 设置锁过期时间,过期自动解锁。 + +如果设置过期时间代码没执行,一样死锁。加锁和设置过期时间要原子操作。 + +``` +set lock 111 EX 300 NX +``` + +还存在的问题: + +* 业务执行时间长,锁自动过期了, +* 把别人的锁删除了 + +解决: + +* 占锁的时候,值指定为uuid,每个人匹配是自己锁才删除。获取锁的值和删除锁也要原子操作。 + +最后存在的问题就是没有锁的自动续期,需要把过期时间设置长一点。 + +## Redisson + +锁的自动续期,不用担心业务时间长,锁被释放。加锁业务完成就不会自动续期。 + +如果指定了锁的过期时间就不会自动续期。 + +## 缓存一致性 + +双写模式,改完数据重新缓存 + +* 脏数据 +* 解锁解决 + +失效模式,改完数据删除缓存 + +* 脏数据 + +终极解决canal + +## spring cache + +Cachemanager 管理多个cache + +* Cacheable:触发将数据保存到缓存的操作 +* CacheEvict: 触发将数据从缓存删除的操作 +* CachePut: 不影响方法执行更新缓存 +* Caching: 组合以上多个操作 +* CacheConfig: 在类级别共享缓存的相同配置 + +``` +@EnableCaching +``` + +### 自定义配置 + +```java +package com.atguigu.gulimall.product.config; + +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author wardseptember + * @create 2020-09-06 14:13 + */ +@EnableConfigurationProperties(CacheProperties.class) +@EnableCaching +@Configuration +public class MyCacheConfig { + + @Bean + public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); + config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + if (redisProperties.getKeyPrefix() != null) { + config = config.prefixKeysWith(redisProperties.getKeyPrefix()); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + return config; + } +} + +``` + +### 存在问题 + +缓存击穿没解决,本地锁 + +缓存雪崩 + +# 异步 + +`CompletableFuture` + +业务场景 + +- 获取SKU基本信息 +- 获取SKU图片信息 +- 获取SKU促销信息 +- 获取SPU所有销售属性 +- 获取规格参数组及组下的规格参数 +- SPU详情 + +``` +public static CompletableFuture runAsync(Runnable runnable, + Executor executor) { +``` + +没有返回值 + +``` + public static CompletableFuture supplyAsync(Supplier supplier, + Executor executor) { + return asyncSupplyStage(screenExecutor(executor), supplier); + } +``` + +有返回值 + + + +```java + @Override + public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException { + SkuItemVo skuItemVo = new SkuItemVo(); + + CompletableFuture infoFuture = CompletableFuture.supplyAsync(() -> { + //1. SKU基本信息获取,pms_sku_info + SkuInfoEntity byId = this.getById(skuId); + skuItemVo.setInfo(byId); + return byId; + },executor); + + CompletableFuture saleFuture = infoFuture.thenAcceptAsync((res) -> { + //3. 获取SPU销售属性组合 pms_product_attr_value + List skuItemSaleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId()); + skuItemVo.setSaleAttr(skuItemSaleAttrVos); + }, executor); + + CompletableFuture descFuture = infoFuture.thenAcceptAsync((res) -> { + //4. 获取SPU的介绍 pms_spu_info_desc + SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId()); + skuItemVo.setDesp(spuInfoDescEntity); + }, executor); + + CompletableFuture baseFuture = infoFuture.thenAcceptAsync((res) -> { + //5. 获取SPU的规格参数信息 + List spuItemAttrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId()); + skuItemVo.setGroupAttrs(spuItemAttrGroupVos); + }, executor); + + CompletableFuture imagesFuture = CompletableFuture.runAsync(() -> { + //2.SKU的图片信息获取,pms_sku_images + List skuImagesEntities = imagesService.getImagesBySkuId(skuId); + skuItemVo.setImages(skuImagesEntities); + }, executor); + + CompletableFuture seckillFuture = CompletableFuture.runAsync(() -> { + R skuSeckillInfo = seckillFeignService.getSkuSeckillInfo(skuId); + if (skuSeckillInfo.getCode() == 0) { + SeckillInfoVo seckillInfoVo = skuSeckillInfo.getData(new TypeReference() { + }); + skuItemVo.setSeckillInfoVo(seckillInfoVo); + } + }, executor); + + + + CompletableFuture.allOf(infoFuture, saleFuture, descFuture, baseFuture, imagesFuture, seckillFuture).get(); + return skuItemVo; + } +``` + +# 短信验证码 + +整合第三方,传入验证码和手机号即可,注册时校验 验证码是否匹配。 + +验证码提供给别的服务进行调用, + +## 接口防刷 + +把验证码存到redis中,key为AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone,值为验证码+当前系统时间 + +每次请求过来都校验一下,当前系统时间和Redis存的时间差,是否大于60秒,大于再发验证码,不大于返回提示消息 + +```java + @GetMapping("/sms/send") + @ResponseBody + public R sendSMS(@RequestParam("phone") String phone) { + + String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); + if (!StringUtils.isEmpty(redisCode)) { + long l = Long.parseLong(redisCode.split("_")[1]); + if (System.currentTimeMillis() - l < 60000) { + return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg()); + } + } + + String code = UUID.randomUUID().toString().substring(0, 5); + String redisStorage = code + "_" + System.currentTimeMillis(); + // 为验证码设置过期时间 + stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, redisStorage, 10, TimeUnit.MINUTES); + + thirdPartyFeign.sendSMSCode(phone, code); + return R.ok(); + } +``` + +# 注册功能 + +把密码用md5加密后加盐 + +用BCryptPasswordEncoder密码加密器, + +```java + @Override + public MemberEntity login(MemberLoginVO memberLoginVO) { + + String account = memberLoginVO.getAccount(); + String password = memberLoginVO.getPassword(); + + // 去数据库查询 + MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper() + .eq("username", account).or().eq("mobile", account)); + if (!ObjectUtils.isEmpty(memberEntity)) { + String passwordDB = memberEntity.getPassword(); + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + if (passwordEncoder.matches(password, passwordDB)) { + return memberEntity; + } else { + return null; + } + } else { + return null; + } + } +``` + +# 社交登录 + +* 引导用户去授权页 +* 认证成功,返回令牌 +* 用令牌获取信息 + +

+ +微博跳回回调页,后台换取access token + +```java + @GetMapping("/oauth2.0/weibo/success") + public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception { + Map map = new HashMap<>(); + map.put("client_id", "1724306451"); + map.put("client_secret", "768b60b62b4ed4d76f694857cd1bbd11"); + map.put("grant_type", "authorization_code"); + map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success"); + map.put("code", code); + HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>()); + if (response.getStatusLine().getStatusCode() == 200) { + String json = EntityUtils.toString(response.getEntity()); + SocialUser socialUser = JSON.parseObject(json, SocialUser.class); + // 获取用户的登录平台,然后判断用户是否该注册到系统中 + R r = memberFeignService.oauthLogin(socialUser); + if (r.getCode() == 0) { + // session 子域共享问题 + MemberResponseVO loginUser = r.getData(new TypeReference() {}); + session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser); + return "redirect:http://gulimall.com"; + } else { + return "redirect:http://auth.gulimall.com/login.html "; + } + } else { + return "redirect:http://auth.gulimall.com/login.html "; + } + } +``` + +换取到access token后,根据uid查询数据库是否有这个用户,如果有,则更新令牌和过期时间;如果没有,则进行注册。 + +## session + +

+ +session不能跨不同域名共享,解决:放大域名作用域 + +```java +@Configuration +public class GulimallSessionConfig { + @Bean + public CookieSerializer cookieSerializer() { + DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); + cookieSerializer.setDomainName("gulimall.com"); + cookieSerializer.setCookieName("GULISEESSION"); + return cookieSerializer; + } + + @Bean + public RedisSerializer springSessionDefaultRedisSerializer() { + return new GenericJackson2JsonRedisSerializer(); + } +} + +``` + +同一个服务,服务多份,session不同步问题 + +不同服务,session不能共享问题 + +解决方案: + +spring session , 用redis存储session + +# 单点登录 + +

+ +微博登录,新浪网也登录了。 + +1. 给登录服务器留下登录痕迹 +2. 登录服务器将token信息重定向到时候,带到url地址中,并且保存到cookie中 +3. 其他系统将token对应的用户保存到自己的session中 + +pt_key=AAJfk79BADBLdHV4fv8qd3SN0FnWpTzyGQdPDnqTII1qAscGaDFl7pmaKzTuQCTC-kX2fEGjaVU;pt_pin=u_4b940e9f6f429; +