• 微服务入门之某硅谷商城


    本文章主要是介绍某谷新出的一个关于微服务视频,终于在不懈努力下,也是大致把视频给跟完了,也大致分享一下整个视频涉及到的技术栈和用到的一些微服务组件吧…

    介绍

    这个项目的前端是一个使用了网上开源项目的商城项目,后端管理页面是采用thymeleaf模板来搭建的,后端采用的技术有SpringBoot2.3.9.RELEASE、MyBatis-plus,SpringCloud Hoxton.SR10, nacos,openfeign,gateway,SpringCache。数据库采用的MySQL和Redis,在搜索功能实现使用了ElasticSearch,MQ采用的是RabbitMq。

    项目功能

    这个商城包含了前端页面和后台管理页面,后台的服务分为以下这些服务:

    • commons: 主要放置OpenFeign的client和一些配置类,比如mq和cache,以及一些pojo实体类和接收入参的Request,R返回类;

    • gateway: 网关,前端的请求都从网关处理并分发,各个服务在新建后会在网关这边新增配置,配置对应的id、uri、predicates。

    • static:这个Api主要满足前端渲染需要的静态资源。

    • carousl: 这个Api主要负责的是轮播图相关的业务。

    • cart: 这个服务主要是负责购物车的业务,主要是负责添加购物车和移除购物车的请求。

    • category:负责商品之间的分类,首页各个商品的分类。

    • collect: 负责商品收藏。

    • order: 主要是负责用户进行下单,需要清除购物车。

    • product: 主要是负责商品的查询、修改、删除之间的业务。

    • user: 主要负责用户的登录、注册和修改业务。

    • search: 主要是负责搜索业务,采用ES进行搜索,不使用MySQL进行搜索。

    • Admin: 主要负责后台管理的业务。

    OpenFeign的使用

    如果当前服务需要调用其他服务的接口通过openFeign跨服务调用,需要在application配置feign,并在主类中通过EnableFeignClients注入相应的Client,在Client中配置对应的接口路径和入参,就可以实现远程调用,下面就是一个例子

    @FeignClient(value = "cart-service")
    public interface CartClient {
    
        /**
         * 检查商品有没有被引用,有取消删除!
         * @param productId
         * @return
         */
        @PostMapping("/cart/check")
        R checkProduct(Integer productId);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ES的使用

    通过导入elasticsearch-rest-high-level-client,再向容器注入RestHighLevelClient这个依赖就可以使用ES进行查询,通过SearchRequest来封装我们的查询条件进行查询,具体结果的解析需要参考Es的返回值进行规则的解析。

    public R search(ProductSearchRequest productSearchRequest) {
            SearchRequest searchRequest = new SearchRequest("product");
            String search = productSearchRequest.getSearch();
            if (StringUtils.isEmpty(search)) {
                // null
                searchRequest.source().query(QueryBuilders.matchAllQuery());
            } else {
                // 不为null
                searchRequest.source().query(QueryBuilders.matchQuery("all", search));
            }
            // 分页查询
            searchRequest.source().from((productSearchRequest.getCurrentPage() - 1) * productSearchRequest.getPageSize());
            searchRequest.source().size(productSearchRequest.getPageSize());
            SearchResponse searchResponse = null;
            try {
                searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException("查询错误");
            }
            // 解析结果集
            SearchHits searchHits = searchResponse.getHits();
            // 获取符合的数量
            long total = searchHits.getTotalHits().value;
            SearchHit[] items = searchHits.getHits();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在每次服务运行的时候,我们需要先把数据库的数据先加载到ES中。

    @Component
    public class ApplicationRunListener implements ApplicationRunner {
    
        @Autowired
        private RestHighLevelClient restHighLevelClient;
    
        @Resource
        private ProductClient productClient;
    
    
        private String indexStr = "";
    
        /**
         * 1. 判断es中的product索引是否存在
         * 2. 不存在,创建
         * 3. 删除原来的数据
         * 4. 查询全部的数据
         * 5. 进行es库的更新
         * @param args
         * @throws Exception
          */
        @Override
        public void run(ApplicationArguments args) throws Exception {
            GetIndexRequest getIndexRequest = new GetIndexRequest("product");
            boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
            //
            if (exists) {
                // 存在,删除
                DeleteByQueryRequest queryRequest = new DeleteByQueryRequest("product");
                // 全部删除
                queryRequest.setQuery(QueryBuilders.matchAllQuery());
                restHighLevelClient.deleteByQuery(queryRequest, RequestOptions.DEFAULT);
    
            } else {
                CreateIndexRequest createIndexRequest = new CreateIndexRequest("product");
                createIndexRequest.source(indexStr, XContentType.JSON);
                restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
            }
    
            List<Product> productList = productClient.list();
            BulkRequest request = new BulkRequest();
    
            ObjectMapper objectMapper = new ObjectMapper();
    
            for (Product product : productList) {
                ProductDoc productDoc = new ProductDoc(product);
                // 用于插入数据的作用
                IndexRequest indexRequest = new IndexRequest("product").id(product.getProductId().toString());
                // 将productdoc转doc
                String json = objectMapper.writeValueAsString(productDoc);
    
                indexRequest.source(json, XContentType.JSON);
                request.add(indexRequest);
            }
            restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    RabbitMq的使用

    在这个项目中,消息队列的使用场景包括后台修改和删除商品的信息、用户进行下单时需要异步将购物车进行清空和减少商品的库存,使用的场景不是特别多。

    @Component
    public class RabbitMqListener {
    
        @Autowired
        private RestHighLevelClient restHighLevelClient;
    
        /**
         * 监听并插入数据
         * @param product 商品数据
         */
        @RabbitListener(
                bindings = {
                        @QueueBinding(
                                value = @Queue(name="insert.queue"),
                                exchange = @Exchange("topic.ex"),
                                key = "product.insert"
                        )
                }
        )
        public void insert(Product product) throws IOException {
            IndexRequest indexRequest = new IndexRequest("product").id(product.getProductId().toString());
            ProductDoc productDoc = new ProductDoc(product);
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(productDoc);
    
            indexRequest.source(json, XContentType.JSON);
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        }
    
        /**
         * 根据商品id删除
         * @param productId
         * @throws IOException
         */
        @RabbitListener(
                bindings = @QueueBinding(
                        value = @Queue(name = "remove.queue"),
                        exchange = @Exchange("topic.ex"),
                        key = "product.remove"
                )
        )
        public void delete(Integer productId) throws IOException {
    
            DeleteRequest deleteRequest = new DeleteRequest("product").id(productId.toString());
            restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    缓存Cache的使用

    本项目很多地方都使用到了Redis做为缓存,包括轮播图、分类、商品详情等,主要用到的注解有@Cacheable、CacheEvict添加缓存和将数据从缓存中移除掉。

    缓存的配置类

    public class CacheConfiguration {
    
    
        //配置缓存manager
        //同类型,多个bean, @Primary 默认生效! 默认缓存时间1小时!  可以选择!
        @Bean
        @Primary
        public RedisCacheManager cacheManagerHour(RedisConnectionFactory redisConnectionFactory){
    
            RedisCacheConfiguration instanceConfig = instanceConfig(1 * 3600L);//缓存时间1小时
            //构建缓存对象
            return RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(instanceConfig)
                    .transactionAware()
                    .build();
        }
    
        //缓存一天配置
        @Bean
        public RedisCacheManager cacheManagerDay(RedisConnectionFactory redisConnectionFactory){
    
            RedisCacheConfiguration instanceConfig = instanceConfig(24 * 3600L);//缓存时间1小时
    
            //构建缓存对象
            return RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(instanceConfig)
                    .transactionAware()
                    .build();
        }
    
    
        /**
         * 实例化具体的缓存配置!
         *    设置缓存方式JSON
         *    设置缓存时间 单位秒
         * @param ttl
         * @return
         */
        private RedisCacheConfiguration instanceConfig(Long ttl){
    
            //设置jackson序列化工具
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer
                    = new Jackson2JsonRedisSerializer<Object>(Object.class);
    
            //常见jackson的对象映射器,并设置一些基本属性
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            objectMapper.registerModule(new JavaTimeModule());
            objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                    ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    
            return RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(ttl)) //设置缓存时间
                    .disableCachingNullValues()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    小评

    本项目是微服务入门的一个好项目,可以涉及到nacos注册、gateway的使用和服务之间如何使用openfeign进行通信,同时可以熟悉SpringBoot和Mybatis-plus的使用,比较适合于一些刚刚学过一些微服务组件,并且拿去实践一下,如果要是对于SpringBoot和MyBatis-plus这两个框架,一些业务就不需要去学习了,只需要学习如何使用OpenFeign和gate-way,业务的一些查询就不需要学习了,查询比较简单。

    项目的对应文档:https://www.wolai.com/atguigu/m4z5zhigfZdUSvfTUJvYZM

  • 相关阅读:
    你做什么工作?
    Redis 提示“Couldn‘t determine DBSIZE!”
    黑磷量子点/无机荧光量子点/石墨烯量子点水凝胶/量子点/纳米水凝胶荧光探针的研究制备
    Windows Ubuntu子系统使用USB教程
    洛谷P1024 [NOIP2001 提高组] 一元三次方程求解(优雅的暴力+二分,干净利落)
    老板招了个有6年经验的测试员,让我见识到了什么是天花板...
    关于电子商务电商供应链项目,不得不说的开放平台电商API接口
    Ruby on Rails Post项目设置网站初始界面
    SpringBoot+Vue+token实现(表单+图片)上传、图片地址保存到数据库。上传图片保存位置自己定义、图片可以在前端回显(一))
    cassandra-Altering of types is not allowed
  • 原文地址:https://blog.csdn.net/zly03/article/details/127936779