# dongmall **Repository Path**: JDLiao/dongmall ## Basic Information - **Project Name**: dongmall - **Description**: 微服务商城 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-06-30 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## **dongmall** ## 介绍 本商城采用springboot2.1.8+springcloud(Greenwich.SR3)框架以及一些常用的微服务中间件 ## 使用中间件以及数据库的版本详情: rabbitmq:3.8.3-management。 nginx:1.19.0 elasticsearch:7.4.2 redis:6.0.5 nacos-server:1.2.0 seata-server1.0 mysql:5.7 ## 使用说明 **本微服务商城分为前台与后台** ### 1、**后台**采用人人开源作为前期开发。 https://gitee.com/renrenio/renren-fast-vue 后台向前台请求数据时,访问URL都带上/api作为标志(http://localhost:88/api/...),再由微服务网关通过nacos服务配置注册中心进行负载均衡转发到相对应的微服务: ```yml spring: cloud: gateway: routes: - id: third_party_route uri: lb://dongmall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?.*),/$\{segment} - id: member_route uri: lb://dongmall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: ware_route uri: lb://dongmall-ware predicates: - Path=/api/ware/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: coupon_route uri: lb://dongmall-coupon predicates: - Path=/api/coupon/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: product_route uri: lb://dongmall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?/?.*),/$\{segment} #http://localhost:88/api/product -> http://localhost:10001/product - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?/?.*), /renren-fast/$\{segment} #http://localhost:88/api/-> http://localhost:10001/renren-fasi ``` ![](images/微信截图_20200729154500.png) ![](images/微信截图_20200729154553.png) ![](images/微信截图_20200729154719.png) ![](images/微信截图_20200729154737.png) ![](images/微信截图_20200729154755.png) ![](images/微信截图_20200729154834.png) ![](images/微信截图_20200729154922.png) ### 2、**前台**采用spring-boot-thymeleaf开发,所有的静态资源全都使用nginx反向代理来管理。 ![](images/微信截图_20200729162043.png) nginx配置: ```conf server { listen 80; listen [::]:80; #拦截域名 server_name dongmall.com *.dongmall.com; location /static/{ root /usr/share/nginx/html; } location /{ proxy_set_header Host $host; proxy_pass http://dongmall; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } ``` 所有前端页面静态资源请求都通过nginx反向代理完成,而其他请求url通过微服务dongmall-gateway网关来进行负载均衡转发到对应的微服务: ```yml spring: cloud: gateway: routes: - id: third_party_route uri: lb://dongmall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?.*),/$\{segment} - id: member_route uri: lb://dongmall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: ware_route uri: lb://dongmall-ware predicates: - Path=/api/ware/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: coupon_route uri: lb://dongmall-coupon predicates: - Path=/api/coupon/** filters: - RewritePath=/api/(?.*),/$\{segment} - id: product_route uri: lb://dongmall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?/?.*),/$\{segment} #http://localhost:88/api/product -> http://localhost:10001/product - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?/?.*), /renren-fast/$\{segment} #http://localhost:88/api/-> http://localhost:10001/renren-fasi - id: dongmall_host_route uri: lb://dongmall-product predicates: - Host=dongmall.com,item.dongmall.com - id: dongmall_search_route uri: lb://dongmall-search predicates: - Host=search.dongmall.com - id: dongmall_auth_route uri: lb://dongmall-auth-server predicates: - Host=auth.dongmall.com - id: dongmall_cart_route uri: lb://dongmall-cart predicates: - Host=cart.dongmall.com - id: dongmall_order_route uri: lb://dongmall-order predicates: - Host=order.dongmall.com - id: dongmall_seckill_route uri: lb://dongmall-seckill predicates: - Host=seckill.dongmall.com ``` ### 页面显示: ![](images/微信截图_20200729185747.png) ![](images/微信截图_20200729185821.png) ![](images/微信截图_20200729185903.png) ![](images/微信截图_20200729185933.png) ![](images/微信截图_20200729185953.png) ![](images/微信截图_20200729190008.png) ![](images/微信截图_20200729190039.png) ### 使用elasticsearch #### 1、批量导入elasticsearch数据: json数据集:https://github.com/elastic/elasticsearch/edit/master/docs/src/test/resources/accounts.json ``` POST /bank/account/_bulk ...json数据集 ``` #### 2、查询: 通过使用REST request URI发送搜索参数(uri+搜索参数): ``` GET bank/_search?q=*&sort=account_number:asc ``` 通过使用REST request body来发送他们(uri+请求体): ``` GET bank/_search { "query": { "match_all": {} }, "sort": [ { "balance": { "order": "desc" } } ], "from": 0, "size": 2 } ``` #### 3、聚合: **搜索address中包含mill的所有人的年龄分布以及平均年龄** ```json GET /bank/_search { "query": { "match": { "address": "MILL" } }, "aggs": { "ageagg": { "terms": { "field": "age", "size": 10 } }, "ageAvg": { "avg": { "field": "age" } }, "balanceAvg": { "avg": { "field": "balance" } } }, "size": 0 #表示不查看命中的详情 } ``` **按照年龄聚合,并且请求这些年龄段的这些人的平均薪资** ``` GET /bank/_search { "query": { "match_all": {} }, "aggs": { "ageAgg": { "terms": { "field": "age", "size": 100 }, "aggs": { "ageAvg": { "avg": { "field": "balance" } } } } }, "size": 0 } ``` **查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资** ``` GET /bank/_search { "query": { "match_all": {} }, "aggs": { "ageAgg": { "terms": { "field": "age", "size": 100 }, "aggs": { "genderAgg": { "terms": { "field": "gender.keyword", "size": 10 }, "aggs": { "balanceAgg": { "avg": { "field": "balance" } } } }, "ageBalanceAgg":{ "avg": { "field": "balance" } } } } }, "size": 0 } ``` #### 4、映射 创建映射: ``` PUT /my_index { "mappings": { "properties": { "age":{"type": "integer"}, "email": {"type": "keyword"}, "name": {"type": "text"} } } } ``` 添加映射: ``` PUT /my_index/_mapping { "properties": { "employee_id": { "type": "keyword", "index": false } } } ``` "index" : false 表示不参与检索,相当于冗余信息 #### springboot整合high-level-client https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high.html ```xml org.elasticsearch.client elasticsearch-rest-high-level-client 7.4.2 1.8 7.4.2 ``` #### 6、编写elasticsearch的配置文件并测试: ```java @Configuration public class DongmallElasticSearchConfig { public static final RequestOptions COMMON_OPTIONS; static { RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); // builder.addHeader("Authorization", "Bearer " + TOKEN); // builder.setHttpAsyncResponseConsumerFactory( // new HttpAsyncResponseConsumerFactory // .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024)); COMMON_OPTIONS = builder.build(); } @Bean public RestHighLevelClient esRestClient(){ RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("192.168.152.132", 9200, "http") )); return client; } } ``` 测试: ```java @Autowired private RestHighLevelClient client; //测试数据到es,更新也行 @Test public void indexData() throws IOException { IndexRequest indexRequest = new IndexRequest("users"); indexRequest.id("1"); //数据的id User user = new User(); user.setAge(18); user.setGender("男"); user.setUserName("zhangsan"); String jsonString = JSON.toJSONString(user); indexRequest.source(jsonString, XContentType.JSON); //要保存的内容 //执行操作 IndexResponse index = client.index(indexRequest, DongmallElasticSearchConfig.COMMON_OPTIONS); //提取有用的响应数据 System.out.println(index); //IndexResponse[index=users,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1//,shards={"total":2,"successful":1,"failed":0}] } @Data class User{ private String userName; private Integer age; private String gender; } ``` **复杂查询**: 搜索address中包含mill的所有人的年龄分布以及平均年龄 ```java @Autowired private RestHighLevelClient client; @ToString @Data static class Account{ private int account_number; private int balance; private String firstname; private String lastname; private int age; private String gender; private String address; private String employer; private String email; private String city; private String state; } @Test public void searchData() throws IOException { //1创建检索请求 SearchRequest searchRequest = new SearchRequest(); //指定索引 searchRequest.indices("bank"); //指定DSL,检索条件 //SearchSourceBuilder 封装的条件 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); //1.1 构造检索条件 // sourceBuilder.query(); // sourceBuilder.from(); // sourceBuilder.size(); // sourceBuilder.aggregation(); sourceBuilder.query(QueryBuilders.matchQuery("address","mill")); //1.2、按照年龄的值分布进行聚合 TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10); sourceBuilder.aggregation(ageAgg); //1、3计算平均薪资 AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance"); sourceBuilder.aggregation(balanceAvg); System.out.println(sourceBuilder.toString()); searchRequest.source(sourceBuilder); //2、执行检索 SearchResponse searchResponse = client.search(searchRequest, DongmallElasticSearchConfig.COMMON_OPTIONS); //3、分析结果 searchResponse System.out.println(searchResponse.toString()); //3.1获取所有查到的数据 SearchHits hits = searchResponse.getHits(); SearchHit[] searchHits = hits.getHits(); for (SearchHit hit : searchHits) { String string = hit.getSourceAsString(); Account account = JSON.parseObject(string, Account.class); System.out.println("account: "+account); } //3.2 获取这次检索的分析信息 Aggregations aggregations = searchResponse.getAggregations(); Terms ageAgg1 = aggregations.get("ageAgg"); for (Terms.Bucket bucket : ageAgg1.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("年龄:"+keyAsString); } //年龄:38 // 年龄:28 // 年龄:32 Avg balanceAvg1 = aggregations.get("balanceAvg"); System.out.println("平均薪资:"+balanceAvg1.getValue());//平均薪资:25208.0 } ``` #### 7、商城业务-商品上架-sku在es中存储模型分析 ``` PUT product { "mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword", "index": false, "doc_values": false }, "saleCount": { "type": "long" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": { "type": "keyword", "index": false, "doc_values": false }, "brandImg": { "type": "keyword", "index": false, "doc_values": false }, "catalogName": { "type": "keyword", "index": false, "doc_values": false }, "attrs": { "type": "nested", "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword", "index": false, "doc_values": false }, "attrValue": { "type": "keyword" } } } } } } ``` "attrs": {"type": "nested",}添加nested,当属性类型为数组对象时,避免被扁平化处理, 访问url:/product/spuinfo/{spuId}/up ![](images/微信截图_20200713184111.png) ### 商城业务-首页-整合thymeleaf渲染首页 1、导入依赖: ```xml org.springframework.boot spring-boot-starter-thymeleaf ``` 2、设置thymeleaf模板引擎关闭缓存,这样每次刷新页面会及时的看到修改后的页面: ```yml spring: thymeleaf: cache: false ``` 3、将html的首页资源拷贝到dongmall-product微服务下 ![](images/微信截图_20200713184552.png) ![](images/微信截图_20200713184647.png) ### 商城业务-首页-整合dev-tools渲染一级分类数据 1、编写controller,当访问默认首页时,自动取出一级目录 ```java @Controller public class IndexController { @Autowired CategoryService categoryService; //首页面跳转 @GetMapping({"/","index.html"}) public String indexPage(Model model){ List categoryEntities = categoryService.getLeve1Categorys(); model.addAttribute("categorys",categoryEntities); return "index"; } } ``` 2、渲染一级目录: ```html
``` 3、导入热部署依赖(使用Ctrl+Shift+F9刷新当前页面) ```xml org.springframework.boot spring-boot-devtools true ``` 4、可看到新的导航栏: ![](images/微信截图_20200713190627.png) ### 商城业务-首页-渲染二级三级分类数据 二三级分类数据是从catalogLoader.js上获取的 ![](images/微信截图_20200713195857.png) 编写controller获取json数据: ```java @GetMapping("/index/catalog.json") @ResponseBody public Map> getCatalogJson(){ Map> map = categoryService.getCatalogJson(); return map; } ``` json数据实体: ```java @Data @AllArgsConstructor @NoArgsConstructor public class Catelog2Vo { private String catalog1Id; private List catalog3List; private String id; private String name; /** * 内部封装的三级分类vo */ @Data @AllArgsConstructor @NoArgsConstructor public static class Catelog3Vo{ private String catalog2Id; private String id; private String name; } } ``` ### nginx反向代理 #### 一、不通过网关 1、在C:\Windows\System32\drivers\etc\hosts下添加域名: ``` 192.168.152.132 dongmall.com ``` 2、在cond.d目录下拷贝一份dongmall.conf文件,并修改如下: ![](images/微信截图_20200713202704.png) 192.168.152.1为windows本机地址 ![](images/微信截图_20200713202844.png) 3、结果如下: ![](images/微信截图_20200713202750.png) 访问顺序:dongmall.com -> nginx ->dongmall-product #### 二、通过网关 1、给conf/nginx.conf添加upstream,实现负载均衡 ![](images/微信截图_20200713205115.png) include /etc/nginx/conf.d/*.conf;表示此文件包含以下目录下的所有以conf结尾的文件。 2、修改conf.d/dongmall.conf如下: proxy_set_header Host $host;表示转发的时候带上host域名(dongmall.com),因为nginx在转发的时候默认不带上。 ![](images/微信截图_20200713205309.png) 3、给dongmall-gateway添加内容: **注意:**要放在其他拦截的下面,否则在使用域名访问其他controller的时候回优先被此拦截。 ```yml - id: dongmall_host_route uri: lb://dongmall-product predicates: - Host=**.dongmall.com,dongmall.com ``` 4、结果如下: ![](images/微信截图_20200713205656.png) 访问顺序:dongmall.com -> nginx -> dongmall-gateway -> dongmall-product ### Jmeter压力测试 #### 1、 JMeter Address already in use错误解决 本身提供的端囗访问机制的问题。 Windows提供给TCP/IP链接的端口为1024:5000,并且要四分钟来循环回收他们。就导致 我们在短时间内跑大量的请求时将端口占满了 1、cmd中,用 regedit命令打开注册表 2、在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters下, + 右击 parameters,添加一个新的 DWORD,名字为 MauSer Port + 然后双击 MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运 行的话,控制机器和负载机器都需要这样操作哦) 3、修改配置完毕之后记得重启机器才会生效 TCPTimedWaitDelay: 30 设置30秒回收 #### 2、性能压测-性能监控-jvisualvm使用 ![](images/微信截图_20200713231802.png) ![](images/微信截图_20200713231842.png) 当更新插件时报错:设置对应版本的URL地址 https://visualvm.github.io/pluginscenters.html ![](images/微信截图_20200713232255.png) ![](images/微信截图_20200713232544.png) 安装Visual GC插件: ![](images/微信截图_20200713232645.png) #### 3、nginx动静分离 1、将dongmall下的static文件拷贝到/data/nginx/html/static,并删除dongmall下的static目录下的文件 ![](images/微信截图_20200714102812.png) 2、将index.html的静态资源URL全部加上static ![](images/微信截图_20200714102905.png) 3、编写dongmall.conf文件内容: ![](images/微信截图_20200714103028.png) root /usr/share/nginx/html;表示当拦截到/static/时,静态资源的根目录为html目录下。 注意:要在location /{}前面先拦截(这个默认拦截所有)。 4、结果: ![](images/微信截图_20200714103246.png) 不能再使用localhost:10001来访问首页了,因为微服务已经没有了静态资源,而nginx只设置为拦截dongmall.com,所以会访问不了静态资源。 #### 5、监控指标 ![](images/微信截图_20200714104532.png) ### 缓存 #### 1、整合redis 1. 导入依赖: ```xml org.springframework.boot spring-boot-starter-data-redis ``` 2. 编写yml ```yml spring: redis: host: 192.168.152.132 port: 6379 ``` 3. 测试: ```java @Autowired StringRedisTemplate stringRedisTemplate; @Test public void testStringRedisTemplate(){ //hello world ValueOperations ops = stringRedisTemplate.opsForValue(); //保存 ops.set("hello","world_"+ UUID.randomUUID().toString()); //查询 String hello = ops.get("hello"); System.out.println(hello);//world_17a8eeb1-6eb2-4bd5-a541-2505eeaea7f9 } ``` ![](images/微信截图_20200714113055.png) #### 2、使用redis缓存改造三级分类 ```java @Override public Map> getCatalogJson() { //给缓存中房json字符串,拿出json字符串,还用逆转能用的对象类型;【反序列化与序列化】 //1、加入缓存逻辑,缓存中存的数据是json字符串 //JSON跨语言,跨平台兼容 String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON"); if (StringUtils.isEmpty(catalogJSON)){ //2、缓存中没有,查询数据库 Map> catalogJsonFromDb = getCatalogJsonFromDb(); //3、查到的数据再放入缓存,将对象转为json放在缓存中 String s = JSON.toJSONString(catalogJsonFromDb); stringRedisTemplate.opsForValue().set("catalogJSON",s); return catalogJsonFromDb; } //转为我们指定的对象 Map> result = JSON.parseObject(catalogJSON, new TypeReference>>() { }); return result; } ``` 原来的getCatalogJson()变为getCatalogJsonFromDb()纯从数据库取。 #### 3、压力测试出的内存泄漏与解决 压测http://localhost:10001/index/catalog.json 1秒50个线程数并一直循环。 ![](images/微信截图_20200714132742.png) 产生了堆外存泄漏:OutDirceMemoryError 1、springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信 2、lettuce的bug导致netty的堆内存溢出 (-Xmx300m 如果没有指定堆外存, 默认使用 -Xmx300) -Dio.netty.maxDirectMemory进行设置 解决方案: 不能单独-Dio.netty.maxDirectMemory进行设置 调大堆外内存 ① 升级lettuce客户端 ② 切换使用jedis 切换jedis: ```xml org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis ``` 可以看到现在就不报错了: ![](images/微信截图_20200714133135-1596013338620.png) #### 4、加本地锁解决缓存击穿问题 将查询数据库加锁:这里的this表示使用的是本地锁,只能锁住当前单例服务,当有多个分布式服务时,就锁不住了,每个分布式服务都会查询一次数据库 注意:查询完数据后先放进缓存,再释放锁。 ```java public Map> getCatalogJsonFromDBWithLocalLock() { // TODO 本地锁 synchronized 进程锁 锁不住分布式的服务 // 只要是同一把锁,就可以锁住需要这个锁的所有线程 // 1、synchronized (this) springboot所有的组件在容器中都是单例的 synchronized (this){ return getCatalogJsonFromDB(); } } public Map> getCatalogJsonFromDB() { ... //3、查到的数据再放入缓存,将对象转为json放在缓存中 String s = JSON.toJSONString(map); stringRedisTemplate.opsForValue().set("catalogJSON",s); return map; } ``` #### 5、分布式锁的使用 http://www.redis.cn/commands/set.html setIfAbsent相当于redis的SETNX,NX表示只有键key不存在的时候才会设置key的值, ```java public Map> getCatalogJsonFromDBWithRedisLock() { // 抢占分布式锁 setIfAbsent --> NX 不存在才占坑 EX 自动过期时间 // 设置redis锁的自动过期时间 - 防止出现异常、服务崩塌等各种情况,没有执行删除锁操作导致的死锁问题 // !!! 设置过期时间和加锁必须是同步的、原子的 String uuid = UUID.randomUUID().toString(); Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); System.out.println(lock); if (lock){ System.out.println("获取分布式锁成功..."); // 加锁成功...执行业务 Map> dataFromDB; try { // 访问数据库 dataFromDB = getCatalogJsonFromDB(); }finally { // 获取对比值和对比成功删除锁也是要同步的、原子的执行 参照官方使用lua脚本解锁 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid); } return dataFromDB; }else{ // 加锁失败休眠一段时间...重试获取锁 System.out.println("获取分布式锁失败...等待重试"); // 重试的频率太快会导致内存溢出 try{ Thread.sleep(200); }catch (Exception e){ e.printStackTrace(); } return getCatalogJsonFromDBWithRedisLock();//自旋的方式 } } ``` #### 6、分布式锁-Redisson简介&整合 https://github.com/redisson/redisson/wiki/Table-of-Content ```xml org.redisson redisson 3.12.0 ``` 编写配置文件: ```java @Configuration public class MyRedissonConfig { /** * 对Redisson的使用都是通过RedissonClient */ @Bean(destroyMethod = "shutdown") public RedissonClient redisson(){ //单Redis节点模式 //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.152.132:6379"); //2、根据config创建RedissonClient示例 RedissonClient redisson = Redisson.create(config); return redisson; } } ``` 测试: ```java @Autowired RedissonClient redissonClient; @Test public void testRedissonClient(){ System.out.println(redissonClient);//org.redisson.Redisson@50785f86 } ``` #### 7、Redisson-lock锁以及看门狗测试 web/indexController.java ```java @ResponseBody @GetMapping("/hello") public String hello(){ //1、获取一把锁,只要锁的名字一样,就是同一把锁 RLock mylock = redisson.getLock("mylock"); //2、加锁 //mylock.lock(); //阻塞式等待,默认加的锁都是30秒 //1)、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉 //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除 mylock.lock(10, TimeUnit.SECONDS);//10秒自动解锁,自动解锁时间一定要大于业务的执行时间 /*问题:mylock.lock(10, TimeUnit.SECONDS);在锁时间到了以后,不会自动续期。 1、如果我们传递了锁的超时时间,就发送给 redis执行脚本,进行占锁,默认超时就是我们指定的时间 2、如果我们未指定锁的超时时间,就使用30*1000【 LockWatchdog Timeout看门狗的默认时间】; 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期 internalLockLeaseTime【看门狗时间】/3,10s 3、最佳实战 Lock.Lock(30, TimeUnit.SECONDS);省掉了整个续期操作。手动解锁 * */ try{ System.out.println(Thread.currentThread().getId() + "---> 加锁成功"); Thread.sleep(30000); }catch (Exception e){ e.printStackTrace(); }finally { //3、解锁,假设锁代码没有运行,redisson不会出现死锁 System.out.println(Thread.currentThread().getId() + " ---> 释放锁"); mylock.unlock(); } return "hello"; } ``` 读写锁测试: ```java //保证一定能读到最新数据,写锁是一个排它锁(互斥锁)。读锁是一个共享锁 //写锁没释放,读就必须等待 //读+读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功 //写+读:等待写锁释放 //写+写:阻塞方式 //读+写:有读写,写锁也必须等待 //只要有写锁在,都必须等待 @GetMapping("/write") @ResponseBody public String writeValue(){ RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); String s = ""; RLock rLock = lock.writeLock(); try { //1、改数据加写锁,读数据加读锁 rLock.lock(); s= UUID.randomUUID().toString(); Thread.sleep(30000); redisTemplate.opsForValue().set("writeValue",s); } catch (InterruptedException e) { e.printStackTrace(); }finally { rLock.unlock(); } return s; } @GetMapping("/read") @ResponseBody public String readValue(){ RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); String s = ""; RLock rLock = lock.readLock(); rLock.lock(); try { s = redisTemplate.opsForValue().get("writeValue"); } catch (Exception e) { e.printStackTrace(); }finally { rLock.unlock(); } return s; } ``` 信号量测试: ```java /** * 车库停车 * 限流 * @return * @throws InterruptedException */ @GetMapping("/park") @ResponseBody public String park() throws InterruptedException { RSemaphore park = redisson.getSemaphore("park"); //park.acquire();//获取一个信号,获取一个值,占一个车位,如果没获得信号,就阻塞等待 boolean b = park.tryAcquire();//尝试获取一个信号,不行就不等了 if (b){ //执行任务 }else{ return "error"; } return "ok"+b; } @GetMapping("/go") @ResponseBody public String go() throws InterruptedException { RSemaphore park = redisson.getSemaphore("park"); park.release(); //释放一个车位 return "ok"; } ``` 闭锁测试: ```java @GetMapping("/lockDoor") @ResponseBody public String lockDoor() throws InterruptedException { RCountDownLatch door = redisson.getCountDownLatch("door"); door.trySetCount(5); door.await(); //等待闭锁都完成,/gogogo/{id}要点击五次 System.out.println("完成"); return "放假了..."; } @GetMapping("/gogogo/{id}") @ResponseBody public String gogogo(@PathVariable("id") Long id) { RCountDownLatch door = redisson.getCountDownLatch("door"); door.countDown(); return id+"班放假了..."; } ``` #### 8、redisson框架实现分布式锁 ```java /** * 查询前台需要显示的分类数据 - redisson框架实现分布式锁 *

* 缓存中的数据如何和数据库的保持一致 * ① 双写模式 :更新完数据更新缓存 * ② 失效模式 :更新完数据删除缓存 * * @return */ public Map> getCatalogJsonFromDBWithRedissonLock(){ // 1、占分布式锁,去reids占坑 RLock lock = redisson.getLock("catalogJson-lock"); lock.lock(); //阻塞等待 //加锁成功...执行业务 Map> dataFromDB; try{ //访问数据库 dataFromDB = getCatalogJsonFromDB(); }finally { lock.unlock();//解锁 } return dataFromDB; } ``` #### 9、缓存-SpringCache-整合&体验@Cacheable **① 引入依赖** 1)、 spring-boot-starter-cache 2)、spring-boot-starter-data-redis **② 写配置** 1)、自动配置了 CacheAutoConfiguration会导入RedisCacheConfiguration 自动配好了缓存管理器 2)、配置使用redis缓存 spring.cache.type=redis **③ 测试使用缓存** @Cacheable: Triggers cache population. -> 触发将数据保存到缓存的操作 @CacheEvict: Triggers cache eviction. -> 触发将数据从缓存删除的操作 @CachePut: Updates the cache without interfering with the method execution. -> 不影响方法执行更新缓存 @Caching: Regroups multiple cache operations to be applied on a method. -> 组合以上多个操作 @CacheConfig: Shares some common cache-related settings at class-level. -> 在类级别共享缓存相同配置 1)、开启缓存功能 @EnableCaching 2)、只需要使用注解就可使用缓存 1. 导入依赖: ```xml org.springframework.boot spring-boot-starter-cache ``` 2. 编写配置: ```properties spring.cache.type=redis # 缓存过期时间,以毫秒为单位 1小时 spring.cache.redis.time-to-live=3600000 ``` 3. 在主启动类下开启缓存功能 ```java @EnableCaching ``` 4. 在查询一级目录下添加缓存注解: ```java /** * 查询所有一级分类 *

* 1、每一个需要缓存的数据我们都要来指定放到哪个名字的缓存;【按照业务类型来划分取名】,可以放多个名字的缓存 * 2、@Cacheable({"category"}),当前方法的结果需要缓存 如果缓存中有,方法不调用; 如果缓存中没有,会调用该方法,最后将方法的结果放入缓存 * 3、默认行为 * 1)、默认缓存不过期 * 4、自定义 * 1)、指定缓存生成指定的key * 2)、指定缓存的过期时间 配置文件修改ttl * 3)、将缓存的value保存为json格式 * * @return */ //sync=true,加本地锁 @Cacheable(value = {"category"}, key = "#root.method.name", sync = true) @Override public List getLeve1Categorys() { System.out.println("CategoryServiceImpl.getLeve1Categorys (获取一级分类)调用了"); List categoryEntityList = baseMapper.selectList(new QueryWrapper().eq("parent_cid", 0)); return categoryEntityList; } ``` #### 10、SpringCache自定义缓存配置 使用默认SpringCache配置时,存储的值不是以JSON形式,所有我们使用自定义配置成JSON格式。 ![](images/微信截图_20200715110126.png) 编写自定义配置文件: ```java 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; @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())); //修改默认存储的值类型为JSON 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; } } ``` application.properties: ```properties spring.cache.type=redis # 以毫秒为单位 1小时 spring.cache.redis.time-to-live=3600000 # 配置缓存名的前缀 如果没配置则使用缓存名作为前缀 #spring.cache.redis.key-prefix=CHCHE_ # 配置前缀是否生效 默认为ture spring.cache.redis.use-key-prefix=true # 是否缓存空值 默认为true spring.cache.redis.cache-null-values=true ``` ![](images/微信截图_20200715111121.png) #### 11、SpringCache-@CacheEvict 使用@CacheEvict,当更新了目录之后,删除对应的缓存 ```java // @Caching(evict = { // @CacheEvict(value = "category", key = "'getLeve1Categorys'"), // @CacheEvict(value = "category", key = "'getCatalogJson'") // }) // allEntries = true 删除category 分区的所有缓存 批量清除 @CacheEvict(value = {"category"}, allEntries = true) @Override public void updateCascade(CategoryEntity category) { //更新本分类数据 this.updateById(category); //更新关联目录数据 categoryBrandRelationService.updateCategory(category.getCatId(),category.getName()); } ``` key使用SPEL表达式,当为字符串时,需要添加单引号 修改三级目录为使用SpringCache: ```java /** * 查询前台需要显示的分类数据 - 使用spring cache框架缓存 * * @return */ @Cacheable(value = {"category"}, key = "#root.methodName", sync = true) @Override public Map> getCatalogJson() { ...查询数据库 } ``` #### 12、SpringCache的不足 1、读模式: ​ 缓存穿透:查询一个null数据。解决:缓存空数据:spring.cache.redis.cache-null-values=true。 ​ 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁,默认是无加锁的:sync=true(本地锁) ​ 缓存雪崩:大量的key同时过期。解决:加随机时间。spring.cache.redis.time-to-live=3600000 2、写模式: + 读写加锁 + 引入Canal,感知到MySQL的更新去更新数据库 + 读多写多,直接去数据库查询就行 总结: ​ 常规数据(都多写少,及时性、一致性要求不高的数据):完全可以使用Spring-Cache。 ​ 特殊数据:特殊设计。 ### 检索服务 #### 1、搭建页面环境 1.1、导入thymeleaf依赖: ```xml org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-devtools true ``` 1.2、修改hosts: ``` 192.168.152.132 search.dongmall.com ``` 1.3、将搜索页前端代码拷贝到/data/nginx/html/static/search/ ![](images/微信截图_20200715141848.png) 1.4、修改index.html,将连接修改为/static/search/ 1.5、修改conf.d下的dongmall.conf文件如下: ![](images/微信截图_20200715142009.png) 1.6、添加网关: ```yml - id: dongmall_search_route uri: lb://dongmall-search predicates: - Host=search.dongmall.com ``` 1.7、访问结果: ![](images/微信截图_20200715142123.png) #### 2、修改页面跳转 dongmall-search:list.html 2.1、跳回主页面 ![](images/微信截图_20200715150322.png) ![](images/微信截图_20200715150632.png) 2.2、主页面搜索框: ```html 118