谷粒商城高级篇

第一章 ElasticSearch

1.1 基本概念

Index索引:

动词:相当于mysql的insert

名词:相当于mysql的database

 

Type类型:

在index中,可以定义一个或多个类型,类似于mysql的table,每一种类型的数据放在一起。

注意新版本不要使用Type了。

 

Document文档:

保存在某个index下,某种type的一个数据document,文档是json格式的,document就像是mysql中的某个table里面的内容。每一行对应的列叫属性。

 

倒排索引:

 

1.2 Docker安装

1.2.1 ElasticSearch

下载镜像

docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2

创建实例

# 将docker里的目录挂载到linux的/mydata目录中
# 修改/mydata就可以改掉docker里的
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data

# es可以被远程任何机器访问
echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml

# 更改权限
chmod -R 777 /mydata/elasticsearch/

# 9200是用户交互端口 9300是集群心跳端口
# -e指定是单阶段运行
# -e指定占用的内存大小,生产时可以设置32G
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e  "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v  /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2 

docker update elasticsearch --restart=always

1.2.2 Kibina

注意下面的ELASTICSEARCH_HOSTS改成自己的实际ip

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.123.65:9200 \
-p 5601:5601 -d kibana:7.4.2

docker update kibana  --restart=always

192.168.123.65:5601 Kibana控制台

1.3 Query DSL

在Kibana的Devtools中 导入测试数据https://github.com/elastic/elasticsearch/blob/master/docs/src/test/resources/accounts.json?raw=true

POST /bank/account/_bulk
粘贴数据

1.3.1 Compound queries

  • boolean query
#must表示必须包含且计算得分
#filter表示必须包含但不计算得分
#should如果单独使用和filter一样,如果不单独使用表示应该满足的条件,不满足也可以,但是满足的话得分更高
GET bank/_search
{
  "query": {
    "bool": {
      "must": {
         "term": {
          "age":32
        }
      },
      "should": {
        "match": {
          "lastname":"Duke"
        }
      }
    }
  }
}
#must_not表示不应该出现,且不计算得分
  • Boosting query
#返回匹配positive查询的文档并降低匹配negative查询的文档相似度分
GET bank/_search
{
  "query": {
    "boosting": {
      "positive": {
        "term": {
          "age":22
        }
      },
      "negative": {
        "term": {
          "gender": "M"
        }
      },
      "negative_boost": 0.2
    }
  }
}
  • Constant score query
#使查询的得分为指定的常量
GET bank/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { 
          "age":22
        }
      },
      "boost": 1.2
    }
  }
}

 

  • Disjunction max query

 

 

1.4 进阶检索

  • 全文检索字段用match。
GET bank/_search
{
  "query": {
    "match": {
      "address": "mill Road"
    }
  }
}

 

  • 其他非text字段匹配用term。
GET bank/_search
{
  "query": {
    "term": {
      "age": "36"
    }
  }
}
  • 字段.keyword:精确匹配
#查不到
GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "mill Road"
    }
  }
}
  • match_phrase:子串包含即可
#查得到
GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "mill Road"
    }
  }
}

 

查出所有年龄分布,并且这些年龄段中男性的平均薪资和女性的平均薪资以及这个年龄段的总体平均薪资

 GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs":{
    "aggAgg":{
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword",
            "size": 10
          },
          "aggs": {
            "balanceAvg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        },
        "ageBalanceAgg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

 

1.4.3 映射

Maping是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。

创建新的映射

PUT /newbank
{
  "mappings": {
    "properties": {
      "account_number": {
        "type": "long"
      },
      "address": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "balance": {
        "type": "long"
      },
      "city": {
        "type": "keyword"
      },
      "email": {
        "type": "keyword"
      },
      "employer": {
        "type": "keyword"
      },
      "firstname": {
        "type": "text"
      },
      "gender": {
        "type": "keyword"
      },
      "lastname": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "state": {
        "type": "keyword"
      }
    }
  }
}

 

将bank中的数据迁移到新的索引中

POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}

1.4.4 分词

安装中文ik分词器

yum instal -y wget
mkdir /mydata/elasticsearch/plugins/ik
cd /mydata/elasticsearch/plugins/ik
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
yum install -y unzip zip
unzip elasticsearch-analysis-ik-7.4.2.zip
cd ..
chmod -R 777 ik
docker restart elasticsearch

测试中文分词器

GET _analyze
{
   "analyzer": "ik_max_word", 
   "text":"我是中国人"
}

1.5 分词安装到nginx

将远程词库放在nginx里

cd /
mkdir -p mydata/nginx
docker run -p 80:80 --name nginx -d nginx:1.10
cd /mydata
docker container cp nginx:/etc/nginx .
docker stop nginx
docker rm nginx
mv nginx conf
mkdir nginx
mv conf nginx

docker run -p 80:80 --name nginx \
 -v /mydata/nginx/html:/usr/share/nginx/html \
 -v /mydata/nginx/logs:/var/log/nginx \
 -v /mydata/nginx/conf/:/etc/nginx \
 -d nginx:1.10

docker update nginx --restart=always

cd /mydata/nginx/html

vi index.html
#  <h1>hello nginx</h1>
mkdir es
cd es
vi fenci.txt
# 黄志成

vi /mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
#粘贴<entry key="remote_ext_dict">http://192.168.123.65/es/fenci.txt</entry>
docker restart elasticsearch

测试访问分词文件

http://192.168.123.65/es/fenci.txt

 

1.6 Springboot整合ES

创建module gulimall-search

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.4.2</elasticsearch.version>
</properties>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>

增加注册中心配置(设置端口号,开启注册发现,排除数据库数据源)

注入客户端:

@Configuration
public class ESConfig {
    public static final RequestOptions COMMON_OPTIONS;

    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        COMMON_OPTIONS = builder.build();
    }

    @Bean
    public RestHighLevelClient esRestClient() {
        RestClientBuilder builder = null;
        // 可以指定多个es
        builder = RestClient.builder(new HttpHost("192.168.123.65", 9200, "http"));
        RestHighLevelClient client = new RestHighLevelClient(builder);
        return client;
    }
}

 

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {
    @Autowired
    RestHighLevelClient client;

    @Data
    class User {
        String userName;
        int age;
        String gender;
    }

    @Test
    public void indexData() throws IOException {
        // 设置索引
        IndexRequest indexRequest = new IndexRequest("users");
        indexRequest.id("1");

        User user = new User();
        user.setUserName("张三");
        user.setAge(20);
        user.setGender("男");
        String jsonString = JSON.toJSONString(user);

        //设置要保存的内容,指定数据和类型
        indexRequest.source(jsonString, XContentType.JSON);

        //执行创建索引和保存数据
        IndexResponse index = client.index(indexRequest, ESConfig.COMMON_OPTIONS);
        System.out.println(index);
    }
}

 

复杂检索:

参考文档https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html

    @Test
    public void find() throws IOException {
        // 1 创建检索请求
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("bank");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 构造检索条件
        sourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
        //AggregationBuilders工具类构建AggregationBuilder
        // 构建第一个聚合条件:按照年龄的值分布
        TermsAggregationBuilder agg1 = AggregationBuilders.terms("agg1").field("age").size(10);// 聚合名称
        // 参数为AggregationBuilder
        sourceBuilder.aggregation(agg1);
        // 构建第二个聚合条件:平均薪资
        AvgAggregationBuilder agg2 = AggregationBuilders.avg("agg2").field("balance");
        sourceBuilder.aggregation(agg2);
        System.out.println("检索条件" + sourceBuilder.toString());
        searchRequest.source(sourceBuilder);
        // 2 执行检索
        SearchResponse response = client.search(searchRequest, ESConfig.COMMON_OPTIONS);
        // 3 分析响应结果
        System.out.println(response.toString());
    }

 

第二章 商城业务

2.1 商品上架

建立index

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"
          }
        }
      }
    }
  }
}

2.2 整合thymeleaf

pom.xml引入

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
  • 将静态资源放到static文件夹中。
  • 将页面放到templates中。
  • 开发期间application.yml配置中关闭thymeleaf缓存。
  • 引入devtools后,修改完页面重新编译当前页面即可实时修改。

 

2.3 Nginx反向代理

流程:

访问gulimall.com -> hosts配置了域名gulimall.com对应的ip为nginx的地址 -> nginx根据host转到gateway -> gateway根据host转到product -> product默认页面记为商城首页。

 

cd /mydata/nginx/conf
#修改nginx.conf
    upstream gulimall{
      server 192.168.123.216:88
    }
cd conf.d
#修改gulimall.conf
    server_name  gulimall.com;
    
    location / {
      proxy_set_header Host $host;
      proxy_pass http://gulimall;
    }

 

Gateway增加host配置,注意一定要放最后面。

        - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
            - Host=**.gulimall.com,gulimall.com

2.4 压力测试

2.4.1 性能指标

  • TPS,每秒处理事务数。
  • QPS,每秒处理查询次数。

主要关注

  • 吞吐量:每秒能处理的请求书、任务数。
  • 响应时间:服务处理一个请求或一个任务的耗时。
  • 错误率:一批请求中结果出错的请求所占比例。

 

 

2.4.2 监控指标

jvisualvm :用来查看jvm信息

jmeter: 压测工具。

压测内容

压测线程数

吞吐量/s

90%响应时间

99%响应时间

nginx

50

13342

4

73

gateway

50

14545

6

24

product简单请求

50

17908

4

8

gateway+简单请求

50

6551

16

43

nginx+gateway+简单请求

50

1367

21

48

首页一级菜单渲染

50

517(db,thymeleaf)

156

286

首页渲染(开缓存,优化数据库,关日志)

50

1191

55

84

三级分类数据获取

50

7.9(db)

8398

9149

三级分类数据获取(优化业务逻辑)

50

168

1505

2497

三级分类数据获取(redis缓存)

50

1660

35

91

首页全量数据获取

50

15.3(静态资源)

 

 

2.5 nginx动静分离

所有静态资源放nginx里。

规则: /static/**所有请求都由nginx直接返回

gulimall.conf加上下面。

    location /static {
      root  /usr/share/nginx/html;
    }

第三章 缓存与分布式锁

3.1 缓存使用

使用分布式缓存来解决本地缓存数据不一致的问题。

3.2 整合redis

product项目pom.xml添加

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

application.yml中添加

spring:
	redis:
    host: 192.168.123.65

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallProductApplicationTests {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void test() {
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("hello", "world_" + UUID.randomUUID().toString());
        String hello = ops.get("hello");
        System.out.println("数据是" + hello);
    }
}

 

安装可视化工具

mac版:

https://github.com/qishibo/AnotherRedisDesktopManager/releases

windows版安装RedisDesktopManager。

 

Springboot2默认使用Lettuce作为操作redis客户端,低版本的Lettuce有堆外内存泄露的bug。

解决方案:

1.升级Lettuce版本

    <properties>
        <lettuce.version>6.0.3.RELEASE</lettuce.version>
    </properties>

2.换成jedis:

redis-starter中排除lettuce,然后引入jedis。

 

3.3 修改三级分类获取

    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        //加入缓存,缓存中的数据是json字符串
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            //缓存中没有,查数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatelogJsonFromDb();
            //查到的数据转为json放在缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON", s);
            return catalogJsonFromDb;
        } else {
            Map<String, List<Catelog2Vo>> res= JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
            return res;
        }
    }

3.4 缓存穿透、缓存雪崩、缓存击穿

  • 空结果缓存、布隆过滤器,解决缓存穿透
  • 设置过期时间(加随机值),解决缓存雪崩
  • 加分布式锁,解决缓存击穿

3.5 分布式锁的演进

3.5.1 阶段一

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if (lock) {
            //加锁成功
            Map<String, List<Catelog2Vo>> res = getData();
            redisTemplate.delete("lock");//删除锁
            return res;
        } else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();//自旋
        }
    }

3.5.2 阶段二

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if (lock) {
            //加锁成功
            redisTemplate.expire("lock", 30, TimeUnit.SECONDS);//设置过期时间
            Map<String, List<Catelog2Vo>> res = getData();
            redisTemplate.delete("lock");//删除锁
            return res;
        } else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();//自旋
        }
    }

3.5.3 阶段三

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //setnx操作是原子性的
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 300, TimeUnit.SECONDS);
        if (lock) {
            //加锁成功
            Map<String, List<Catelog2Vo>> res = getData();
            redisTemplate.delete("lock");//删除锁
            return res;
        } else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();//自旋
        }
    }

3.5.4 阶段四

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            //加锁成功
            Map<String, List<Catelog2Vo>> res = getData();
            // redisTemplate.delete("lock");//删除锁
            String lockVal = redisTemplate.opsForValue().get("lock");
            if (lockVal.equals(uuid)) {
                redisTemplate.delete("lock");//删除锁
            }
            return res;
        } else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();//自旋
        }
    }

3.5.5 阶段五

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            //加锁成功
            Map<String, List<Catelog2Vo>> res;
            try {
                res = getData();
            } finally {
                String lockVal = redisTemplate.opsForValue().get("lock");
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                //删除锁
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return res;
        } else {
            //加锁失败...重试
            //休眠100ms重试
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();//自旋
        }
    }

3.6 整合redisson

pom.xml增加

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.15.2</version>
        </dependency>

之前application.yml中已经配置过redis地址了。

测试:

    @GetMapping("/hello")
    public String hello() {
        RLock lock = redissonClient.getLock("my-lock");
        lock.lock();//阻塞式等待,默认30s到期时间
        //锁会自动续期,每隔10s续期到30s。业务执行完成后,不会再续期。
        //最佳实践:
        //lock.lock(30,TimeUnit.SECONDS); 不会自动续期
        try {
            System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (Exception e) {

        }finally {
            System.out.println("释放锁..." + Thread.currentThread().getId());
            lock.unlock();
        }
        return "hello";
    }

3.7 缓存一致性

使用分布式锁获取三级分类

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //锁的粒度越细越好
        RLock lock = redissonClient.getLock("catalogJson-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> res;
        try {
            res = getData();
        } finally {
            lock.unlock();
        }
        return res;
    }

此时存在数据一致性问题。

解决方案:

1、缓存的所有数据都有过期时间,数据过期下一次查询主动更新。

2、读写数据的时候加上分布式的读写锁。(只要有写操作,就加锁)

3.8 spring cache简化开发

pom.xml增加(starter-redis之前已经加过了)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>s

自定义配置,使用json的方式存储到redis中。

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
    @Bean
    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;
    }
}

 

application.yml增加使用redis缓存

spring:
  cache:
    type: redis
    redis:
      time-to-live: 3600000
      use-key-prefix: true
      #防止缓存穿透
      cache-null-values: true
      

主启动类增加注解 @EnableCaching

 

加缓存直接在需要缓存的方法上加 @Cacheable注解即可。

    @Cacheable(value = "category", key = "#root.method.name", sync = true)
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        /**
         * 1.空结果缓存,解决缓存穿透
         * 2.设置过期时间(加随机值),解决缓存雪崩
         * 3.加锁,解决缓存击穿
         * */
        //加入缓存,缓存中的数据是json字符串
        return getCatelogListFromDb();
        //  return getCatalogJsonFromDbWithRedissonLock(); 用spring-cache即可,没必要用分布式锁
    }

删缓存,使用@CacheEvict

    @CacheEvict(value = "category", allEntries = true)//将category分区的全部删除
    @Override
    @Transactional
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

 

Spring-cache总结:

1)读模式:

缓存穿透:查询一个null数据。 通过cache-null-values: true解决。

缓存击穿:大量并发进来查询一个正好过期的数据。解决:加锁sync = true (单机锁)。

缓存雪崩:大量key同时过期。解决,加过期时间。time-to-live: 3600000

 

2)写模式:

读写加锁。

引入 Canal

读多写多,直接去数据库查询即可。

 

 

对于常规数据(读多写少,及时性,一致性要求不高的数据),完全可以直接使用spring-cache。

对于特殊数据,特殊设计。

 

第四章 商城业务

4.1 检索服务

4.1.1 准备

将搜索页的index.html放到gulimall-search项目的templates文件夹。其他静态资源放到/mydata/nginx/html/static/search文件夹。

 

pom.xml增加

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

 

增加hosts配置,将search.gulimall.com配置到nginx地址。

192.168.123.65  search.gulimall.com

修改nginx配置gulimall.conf,将所有*.gulimall.com,如果以/static开头则,则去html路径寻找静态文件。其他的路由到上游服务器gateway上去。

    listen       80;
    server_name  gulimall.com *.gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;
    location /static {
      root  /usr/share/nginx/html;
    }

    location / {
      proxy_set_header Host $host;
      proxy_pass http://gulimall;
    }

配置gateway路由规则

        - id: gulimall_search_route
          uri: lb://gulimall-search
          predicates:
            - Host=search.gulimall.com

 

打通首页到搜索页的跳转。

 

4.1.2 数据迁移

之前的索引设计有问题,重新创建索引,并迁移数据。

PUT gulimall_product
{
  "mappings": {
    "properties": {
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      },
      "brandId": {
        "type": "long"
      },
      "brandImg": {
        "type": "keyword"
      },
      "brandName": {
        "type": "keyword"
      },
      "catalogId": {
        "type": "long"
      },
      "catalogName": {
        "type": "keyword"
      },
      "hasStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "saleCount": {
        "type": "long"
      },
      "skuId": {
        "type": "long"
      },
      "skuImg": {
        "type": "keyword"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "spuId": {
        "type": "keyword"
      }
    }
  }
}


POST _reindex
{
  "source": {
    "index": "product"
  },
  "dest": {
    "index": "gulimall_product"
  }
}

修改EsConstant.java中的索引常量

    public static final String PRODUCT_INDEX = "gulimall_product";

4.1.3 DSL语句

GET /gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "华为"
          }
        }
      ],
       "filter": [
        {
          "term": {
            "catalogId": "225"
          }
        },
        {
          "terms": {
            "brandId": [
              "6"
            ]
          }
        },
        {
          "term": {
            "hasStock": "false"
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 1000,
              "lte": 7000
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "1"
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 5,
  "highlight": {
    "fields": {
      "skuTitle": {}
    },
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  },
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": { 
        "brandNameAgg": { 
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
      
        "brandImgAgg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
        
      }
    },
    "catalogAgg":{
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalogNameAgg": {
          "terms": {
            "field": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attrs":{
      "nested": {"path": "attrs" },
      "aggs": {
        "attrIdAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

4.1.4 SearchRequest构建

 

4.2 异步和线程池

4.2.1 线程回顾

初始化线程的4种方式:

  1. 继承Thread
new Thread01().start();
  1. 实现Runnable接口
new Thread(new Runable01()).start();
  1. 实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常)
    public static class Callavble01 implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果" + i);
            return i;
        }
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main .... start......");
        FutureTask<Integer> futureTask = new FutureTask<>(new Callavble01());
        new Thread(futureTask).start();
        //阻塞等待整个线程执行完成,获取返回结果
        Integer integer = futureTask.get();
        System.out.println("main.....end......" + integer);
    }
  1. 线程池

两种方式:

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,
                50, 
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000), 
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

 

4.2.2 线程池七大参数

  • corePoolSize:核心线程数,一直存在。除非设置了allowCoreThreadTimeOut。
  • maximumPoolSize:最大线程数量。
  • keepAliveTime:存活时间。如果当前线程的数量大于core的数量,只要空闲的线程时间大于指定存活时间,释放空闲的线程(maximumPoolSize-corePoolSize)
  • unit :时间单位
  • workQueue 阻塞队列。如果任务有很多,会将多的任务放阻塞队列里,只要有线程空闲,就会去队列里取任务执行。
  • threadFactory:线程的创建工厂
  • handler :线程满了,按照指定的拒绝策略拒绝执行任务。

 

工作顺序:

1)线程池创建,准备好core数量的核心线程,准备接受任务。

1.1) core满了,再进来的任务进阻塞队列。空闲的core会去阻塞队列取任务执行。

1.2) 阻塞队列满了,直接开新线程执行,最大只能开到max数量。

1.3) max满了,用拒绝策略拒绝任务。

1.4) max都执行完成,有很多空闲,在指定时间后,释放max-core的线程。

 

一个线程池core:7,max:20, queue:50, 如果100并发进来如何分配。

7个立即执行,50个进队列,再开13个继续执行,剩余的30个执行拒绝策列。

 

4.2.3 CompletableFuture

 

  • 创建异步操作。

runXXX没有返回值,supplyXXX有返回值。

可以传入自定义线程池,否则使用默认的。

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)

以第四个为例:

public class ThreadTest {
    public static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10,
            50,
            10,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(100000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 1;
        }, threadPool);
        System.out.println(future.get());
    }
}
  • 计算完成时回调

whenComplete是当前执行的线程继续执行whenComplete的任务。

whenCompleteAsync是把任务提交给线程池执行。

exceptionally是在异常后的处理。

    public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
    public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
    public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)
    public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            int i = 10 / 0;
            return i;
        }, threadPool).whenComplete((res, exception) -> {
            System.out.println("结果是" + res + "异常是" + exception);
        }).exceptionally(throwable -> {
            return 10;
        });
        System.out.println(future.get());
  • handle方法

方法执行完成后的处理

        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            int i = 10 / 0;
            return i;
        }, threadPool).handle((res,exception) ->{
            if (res != null) {
                return res;
            }
            if (exception != null) {
                return 0;
            }
            return -1;
        });
        System.out.println(future.get());
  • 线程串行化

thenApply:当一个线程依赖上一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。

thenAccept:接受上一个任务返回的结果,处理,无需返回值。

thenRun:上一个任务完成后,开始执行自己的处理,无需返回值。

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor) 
public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor)  
  • 多任务组合

allOf:等待所有任务完成。

anyOf:只要有一个任务完成。

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

举例:

CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, 
                        baseAttrFuture, imageFuture).get();

4.3 商品详情

4.3.1 环境配置

hosts中增加 192.168.123.65  item.gulimall.com

 

修改网关配置

        - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
            - Host=gulimall.com,item.gulimall.com

 

将商品详情页item.html放入product项目templates文件夹中。

修改html中的静态资源路径。

 

4.3.2 异步编排

pom.xml增加spring源数据处理器,可以有提示.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

创建MyThreadConfig

@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties properties) {
        return new ThreadPoolExecutor(properties.getCoreSize(),
                properties.getMaxSize(),
                properties.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

创建ThreadPoolConfigProperties

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

application.properties增加

gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10

异步编排:

    @Override
    public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
        SkuItemVo skuItemVo = new SkuItemVo();
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            //1、sku基本信息获取  pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);
        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //3、获取spu销售属性组合
            List<SkuItemVo.SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);
        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
            //4、获取spu介绍  pms_spu_info_desc
            SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesp(spuInfoDescEntity);
        }, executor);
        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            //5、获取spu规格参数信息
            List<SkuItemVo.SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);

        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            //2、sku图片信息  pms_sku_images
            List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);
        //等待所有任务都完成
        CompletableFuture.allOf(infoFuture, saleAttrFuture, descFuture, baseAttrFuture, imageFuture).get();
        SeckillInfoVo seckillInfoVo = new SeckillInfoVo();
        skuItemVo.setSeckillInfoVo(seckillInfoVo);
        return skuItemVo;
    }

4.4 认证服务

4.4.1 环境搭建

增加模块gulimall-auth-server。配置好nacos,端口号。开启服务发现,feignClients。

hosts增加认证中心域名 auth.gulimall.com

将登录页和注册页的静态资源放到nginx服务器上。

将登录页和注册页html放到templates文件夹里,修改静态资源路径。

gateway增加配置

        - id: gulimall_auth_route
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.gulimall.com

创建GulimallWebConfig。如果只需要请求跳转页面的,不需要写controller,直接在视图映射中添加跳转逻辑。

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

 

4.4.2 短信接口

验证码防刷

auth-server引入redis,配置好redis的地址和端口。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

将验证码存在redis中,key为手机号,value为验证码_时间戳。再次发验证码时,判断两次时间是否大于一分钟。

    @ResponseBody
    @RequestMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone) {
        //TODO 接口防刷
        String redisCode = redisTemplate.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(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        String code = UUID.randomUUID().toString().substring(0, 5);
        String subString = code + "_" + System.currentTimeMillis();
        //redis缓存验证码
        redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, subString, 5, TimeUnit.MINUTES);
        thirdPartyFeignService.sendCode(phone, code);
        return R.ok();
    }

4.4.3 注册和登录功能

auth-server注册时发远程调用member服务进行注册。

密码注意加密加盐处理

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

 

登录时判断使用passwordEncoder.matches判断密码是否正确

            String password = vo.getPassword();//页面提交的明文
			String passwordDb = entity.getPassword();//数据库密文
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            boolean matches = passwordEncoder.matches(password, passwordDb);

4.4.4 OAuth2.0

 

 

 

用户 浏览器 gulimall 微博

4.4.5 SpringSession

auth-server项目增加依赖

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

增加配置

#spring session配置
spring.session.store-type=redis
server.servlet.session.timeout=30m

主启动类增加注解@EnableRedisHttpSession

 

自定义Configuration解决域名共享问题和json序列化。

@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

4.4.6 退出功能

    @GetMapping("/logout")
    public String login(HttpSession session) {
        if (session.getAttribute(AuthServerConstant.LOGIN_USER) != null) {
            log.info("\n[" + ((MemberRsepVo) session.getAttribute(AuthServerConstant.LOGIN_USER)).getUsername() + "] 已下线");
        }
        session.invalidate();
        return "redirect:http://auth.gulimall.com/login.html";
    }

4.4.7 SSO单点登录

 

4.5 购物车

4.5.1 准备

创建gulimall-cart模块。

hosts增加cart.gulimall.com配置。

静态资源放到nginx, html放到项目中。

配置网关,开启服务发现,feign客户端,排除数据库数据源。

 

4.5.2 拦截器interceptor

很多项目中需要在代码中使用当前登录用户的信息,但是又不方便把保存用户信息的session对象传来传去,这种情况下,就可以考虑使用 ThreadLocal。

增加配置,所有请求都会触发拦截器工作。

@Configuration
public class GuliMallWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

不管是否登录,一定要给浏览器的cookie中添加一个user-key,这样就可以实现未登录用户登录之后,购物车的合并功能。

 

public class CartInterceptor implements HandlerInterceptor {

    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberRsepVo user = (MemberRsepVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (user != null) {
            // 用户登录了
            userInfoTo.setUsername(user.getUsername());
            userInfoTo.setUserId(user.getId());
        }
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setHasSetUserKey(true);
                    break;
                }
            }
        }
        // 不管是否登录都要分配一个userKey
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString().replace("-", "");
            userInfoTo.setUserKey("gulimall-" + uuid);
        }
        threadLocal.set(userInfoTo);
        return true;
    }

    /**
     * 执行完毕之后分配临时用户让浏览器保存
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserInfoTo userInfoTo = threadLocal.get();
        /**
         * 不管是否登录,一定要给浏览器的cookie中添加一个user-key
         * isHasSetUserKey()为true说明cookie中已经添加了user-key
         * isHasSetUserKey()为false,说明cookie中还没添加user-key
         * cookie中添加了user-key之后,下次请求就可以从找到,然后setHasSetUserKey(true)
         */
        if (!userInfoTo.isHasSetUserKey()) {
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            // 设置这个cookie作用域 过期时间
            cookie.setDomain("gulimall.com");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIME_OUT);
            response.addCookie(cookie);
        }
    }
}

 

4.6 消息队列

异步

解耦

削峰

4.6.1 RabbitMQ介绍

4.6.2 RabbitMQ安装

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

docker update rabbitmq --restart=always

4.6.3 SpringBoot整合RabbitMQ

 

增加maven依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

主启动类增加注解@EnableRabbit

配置文件增加配置

spring.rabbitmq.host=192.168.123.65
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/

4.6.4 可靠投递

 

 

 

#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步方式优先回调returnconfirm
spring.rabbitmq.template.mandatory=true

#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual

4.6.5 如何保证消息可靠性

1、消息丢失

ConfirmCallback,发布者消息到达broker,就会执行这个回调,表示服务器接收到了这个消息,但不一定投递到目标队列。把之前发的消息都存起来,并记录是否被服务器收到,定期扫描数据库进行重发。

broker必须持久化,防止宕机的时候消息丢失。

ReturnCallback,只要交换机没有投递给指定的队列,触发这个回调,修改这个消息的状态。

ack确认机制,开启手动确认模式,程序成功处理了,执行ack,没有成功处理,执行nack重新入队,若程序宕机了,也会重新入队。

2、消息重复

如果消费成功ack时宕机了,导致消息重新入队,可能会造成消息重复问题。

业务消费接口应设置成幂等性,比如设置一个状态标志位,之前已处理过的话就不要再处理了。

3、消息积压

上线更多的消费者,或者将消息取出来放数据库,慢慢处理。

 

4.7 订单服务

4.7.1 准备

静态资源放到nginx

页面放到order应用

hosts增加配置 order.gulimall.com

gateway增加配置

        - id: gulimall_order_route
          uri: lb://gulimall-order
          predicates:
            - Host=order.gulimall.com

整合spring session

4.7.2 订单基本概念

订单流程:

4.7.3 Feign远程调用丢失请求头问题

解决

@Configuration
public class GuliFeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //RequestContextHolder拿到刚进来的这个请求
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = requestAttributes.getRequest();
                //同步请求头
                template.header("Cookie", request.getHeader("Cookie"));
            }
        };
    }
}

4.7.4 Feign异步调用丢失请求头的问题

解决:先把RequestAttributes取出来,在异步的时候重新set进去。

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //远程查询所有收货列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRsepVo.getId());
            vo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //远程查询所有购物车选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            vo.setItems(items);
        }, executor);

4.7.5 接口幂等性

接口幂等性就是用户对同一操作发起的一次请求或多次请求的结果是一致的。

 

  • 哪些情况需要防止?

用户多次点击按钮。

用户页面回退再次提交。

微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制。

 

解决方案:

  • token机制

服务端提供发送token的接口,在执行业务前,先获取token,服务器把token存到redis中。调用业务接口时,把token放到请求头。服务器判断token是否存在redis中,存在表示第一次请求,先删除token,再执行业务。

token的获取、比较和删除必须是原子性。可以使用lua脚本。

        String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        //原子验证令牌和删除
        Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRsepVo.getId()), orderToken);
        if (res == 0) {
            //验证失败
        } else {
            //验证成功
        }

 

  • 各种锁机制

数据库悲观锁:select * from tab1 where id = 1 for update; 注意id必须是主键或唯一索引。

数据库乐观锁:update t_goods set count=count-1,version=version+1 where good_id=1 and version=1

业务层分布式锁:处理数据的时候先获取锁,再判断是否被处理过,处理完成后释放锁。

  • 各种唯一约束

数据库唯一约束:插入数据按照唯一索引插入,要求主键不是递增,业务生成唯一主键。在分库分表的情况下,路由规则要保证相同请求落在同一个库和同一个表。

redis防重:很多数据需要被处理,只能处理一次。计算数据的md5放在redis的set中,每次处理数据,先看这个md5是否存在,存在就不处理。

  • 防重表

使用订单号作为防重表的唯一索引,把唯一索引插入防重表,再进行业务操作。且他们在同一个事务中,如果业务失败了,防重表也会回滚。

  • 全局请求唯一id

调用接口时,生成一个唯一id,redis将数据保存到集合中去重。

使用nginx设置每一个请求的唯一id。

proxy_set_header X-Request-Id $request_id

 

4.7.6 下单

 

锁库存:

更详细的内容见4.9.2 锁库存增强版

 

4.7.7 本地事务失效的问题

    @Transactional(timeout = 30)
    public void a() {
        //b c设置都没用,都和a共用一个事务
        b();
        c();
    }

    @Transactional(propagation = Propagation.REQUIRED, timeout = 2)
    public void b() {

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void c() {

    }

同一个对象内事务方法互调默认失效,原因绕过了代理对象,事务使用代理对象来控制的。

解决:使用代理对象来调用事务方法。

引入aop

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

主启动类增加注解@EnableAspectJAutoProxy(exposeProxy = true),开启aspectj动态代理功能

使用代理对象调用

   @Transactional(timeout = 30)
    public void a() {
        //b c设置都没用,都和a共用一个事务
        OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
        orderService.b();
        orderService.c();
    }

    @Transactional(propagation = Propagation.REQUIRED, timeout = 2)
    public void b() {

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void c() {

    }

 

4.7.8 本地事务在分布式下的问题

4.8 分布式事务

4.8.1 为什么有分布式事务

4.8.2 CAP定理与BASE理论

4.8.3 分布式事务的几种方案

4.8.4 seata

 

1、给每个库增加undo_log表

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2、安装事务协调器,seata-server

3、整合

common项目引入seata

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
        </dependency>

下载seata0.7.1版本(与pom中版本对应),修改配置文件registry.conf,将注册中心设置为nacos。启动seata.

 

所有想要用分布式事务的微服务使用seata DataSourceProxy代理数据源

@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

每个微服务必须导入file.conf registry.conf。

修改file.conf vgroup_mapping.{application.name}-fescar-service-group = "default"

给分布式事务的大入口标注@GlobalTransactional,每一个远程的小事务用@Transactional

 

4.9 订单服务

4.9.1 RabbitMQ延时队列(实现定时任务)

 

 

 

4.9.2 锁库存增强版和关闭订单

  • 解锁库存的场景:

1、下订单成功,订单过期没有支付自动取消或者用户手动取消,解锁库存

2、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚,为了保持分布式事务,之前锁定的库存需要解锁。

 

锁库存的时候每一个工作单详情都发给延时队列,工作单详情中记录了某个sku在哪个仓库锁了多少件库存。

库存解锁服务收到了延时队列的消息后。要根据这个工作单详情的id去查数据库。

有这个工作单详情:说明库存锁定成功了,再查这个订单的状态。

无这个订单:必须解锁库存。

有这个订单:根据订单状态,如果已取消则解锁库存,否则不能解锁。

 

无这个工作单详情:说明库存都锁定失败了,库存服务回滚了,无需解锁。

 

  • 关单的场景:

关单服务收到延时队列的消息后,判断订单的状态,只有未付款状态才能关单。

由于各种网络卡顿原因,关单的时候可能比解锁库存延时队列的时间还要晚,如果不主动触发解锁库存,会造成库存永远锁定。在关单的时候发一个实时的消息队列,库存服务根据工作单详情的状态判断,如果还处于锁定状态则要解锁库存。

 

第五章 商城业务

5.1 支付

私钥加签,公钥验签

 

5.1.2 内网穿透设置

nginx增加配置

 
    server_name 38e630e156.qicp.vip; #增加外网穿透的地址
    
    location /payed/notify {
      proxy_set_header Host order.gulimall.com;
      proxy_pass http://gulimall;
    }

5.1.3 收单

 

5.2 秒杀

5.2.1 秒杀业务

秒杀具有瞬间高并发的特点,针对这一特点,必须要做限流+异步+缓存(页面静态化)+独立部署。

 

限流方式:

1.前端限流,比如验证码设计。

2.nginx限流,直接负载部分请求到错误的静态页面,令牌算法、漏斗算法。

3.网关限流,限流的过滤器。

4.代码中使用分布式信号量。

5.rabbitmq限流。

 

 

5.2.2 整合定时任务与异步任务

定时任务:

@EnableScheduling开启定时任务

@Scheduled开启一个定时任务

自动配置类:TaskSchedulingAutoConfiguration

 

异步任务:

@EnableAsync开启异步任务功能

@Asyncg给希望异步执行的方法上标注

自动配置类:TaskExecutionAutoConfiguration

@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class HelloSchedule {
    @Async
    @Scheduled(cron = "* * * ? * 5")
    public void hello() throws InterruptedException {
        log.info("hello," + Thread.currentThread().getId());
        Thread.sleep(3000);
    }
}

设置异步任务线程池大小

spring.task.execution.pool.core-size=20
spring.task.execution.pool.max-size=50

5.2.3 定时任务秒杀商品上架

使用分布式锁保证幂等性,且在执行业务之前先判断redis中是否存在key,不存在才执行业务存入redis中。

 

5.2.4 秒杀系统设计

5.3 Sentinel

5.3.1 熔断、降级、限流

  • 熔断:A调用B,由于各种原因B故障了,直接返回降级数据。
  • 降级:高峰时期,有策略的对一些服务停止服务,返回降级数据,以保证核心业务的正常运行。
  • 限流:对服务的请求流量进行控制。

 

5.3.2 整合Springboot

common项目pom.xml增加

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

查看sentinel版本,下载对应的dashboard控制台 https://github.com/alibaba/Sentinel/releases

 

启动控制台 java -jar sentinel-dashboard-1.6.3.jar --server.port=8333

 

所有项目增加配置

spring.cloud.sentinel.transport.dashboard=localhost:8333

management.endpoints.web.exposure.include=*

feign.sentinel.enabled=true

自定义限流提示语

@Configuration
public class SeckillSentinelConfig {
    public SeckillSentinelConfig() {
        WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
            @Override
            public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
                R error = R.error(BizCodeEnume.TOO_MANY_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_EXCEPTION.getMsg());
                httpServletResponse.setCharacterEncoding("UTF-8");
                httpServletResponse.setContentType("application/json");
                httpServletResponse.getWriter().write(JSON.toJSONString(error));
            }
        });
    }
}

使用Sentinel来保护feign远程调用

1)、调用方的熔断保护:feign.sentinel.enabled=true

2)、调用方手动指定远程服务的降级策略。远程服务被降级处理。触发我们的熔断回调方法。

3)、超大浏览的时候,必须牺牲一些远程服务。在服务提供方指定降级策略,返回默认的熔断数据。

 

5.3.3 自定义受保护的资源

基于代码:

        try (Entry entry = SphU.entry("seckillSkus")) {
          //业务逻辑
        } catch (BlockException e) {
            log.error("资源被限流,{}", e.getMessage());
        }

基于注解:

    public List<SeckillSkuRedisTo> blockHandler(BlockException e) {
        log.error("getCurrentSeckillSkusResource被限流了...");
        return null;
    }

    @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        //业务逻辑
        return null;
    }

 

5.3.4 网关限流

gateway项目增加依赖

		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
			<version>2.1.0.RELEASE</version>
		</dependency>

增加自定义限流提示语

public class SentinelGatewayConfig {
    public SentinelGatewayConfig() {
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            //Mono返回0个或1个数据
            //Flux返回0个或多个数据
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                //网关限流了请求,就会调用此回调
                R error = R.error(BizCodeEnume.TOO_MANY_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_EXCEPTION.getMsg());
                String errJson = JSON.toJSONString(error);
                return ServerResponse.ok().body(Mono.just(errJson), String.class);
            }
        });
    }
}

5.4 Sleuth+Zipkin

5.4.1 介绍

 

基本术语:

 

5.4.2 整合

common项目引入依赖

 	    <properties>
        <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
   	  </properties>		

				<!--链路追踪 包含zipkin+sleuth-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

     <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

虚拟机安装zipkin

docker run -d -p 9411:9411 openzipkin/zipkin

docker update openzipkin --restart=always

所有微服务增加配置

spring.zipkin.base-url=http://192.168.123.65:9411
spring.zipkin.discovery-client-enabled=false
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1

http://192.168.123.65:9411/ zipkin可视化界面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值