springcloud 实战(2)

152 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

前言

上文我们已经搭建了SpringCloud-Eureka进行服务注册发现SpringCloud-Hystrix服务容错, 这里我们继续进行秒杀系统的准备,网关的路由能力,由Zuul和Eureka整合起来以后利用框架Ribben的微服务基础框供;网关的限流能力,主要在Zuul的过滤器功能实现。

zuul网关

秒杀系统需要在内部网关(Zuul)完成认证、负载均衡、接口限流,等等功能,因为时间关系这里就只借用zuul实现负载均衡和接口限流两部分

负载均衡

首先我们搭建一个应用 zuul网关 引入依赖:

<!--Zuul-->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

在启动类的上方加入注解@EnableZuulProxy 在yml中加入配置

ing:
  application:
    name: zuul-service
server:
  port: 9527
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8081/eureka

zuul:
  ribbonIsolationStrategy: THREAD
  host:
    connect-timeout-millis: 60000
    socket-timeout-millis: 60000
  #路由规则
  routes:
    route1:
      path: /test01/**
      serviceId: eureka-client

然后启动server client1,2和zuul服务 打开Eureka可以看到服务全部注册成功

屏幕快照 2022-04-26 下午11.57.15.png 我们有一个网关服务zuul,一个注册中心eureka,2个服务提供者eurekaservice 我们在两个提供的服务的eurekaservice都配置了相同的应用名称

spring:
  application:
    name: eureka-client

根据路由规则就会轮流访问 刚刚部署的两个服务,这样负载均衡就搭建完了。

屏幕快照 2022-04-27 上午12.01.05.png

屏幕快照 2022-04-27 上午12.01.14.png

数据库层面

因为此次我们只是简单的设计一个demo,我们就在orderservice的应用中完成数据库,Kakfka,以及redis的连接和查询,实际上分布式锁和队列以及数据库应用应该分开搭建微服务。 这里首先引入数据库的pom

<!-- jpa -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>${spring.boot.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>${jpa.version}</version>
</dependency>
<!-- Druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid.starter.version}</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid.starter.version}</version>
</dependency>

数据库连接池是必不可少的,这里选用了阿里巴巴的druid,阿里的druid的优势是改造简单以及监控更加强大,对于秒杀活动来说,阿里的连接池也是非常值得信任并且久经考验的。 在yml中加入数据库设置

jpa:
  show-sql: true
datasource:
  url: jdbc:mysql://lo:33064/dolphin?useUnicode=true&characterEncoding=utf-8&useSSL=false&rewriteBatchedStatements=true
  username: dolphin
  password: a7Af956eVd9w76En4p91Wg30B
  driver-class-name: com.mysql.jdbc.Driver
  druid:
    #初始化大小
    initialSize: 5
    #最小值
    minIdle: 5
    #最大值
    maxActive: 20
    #最大等待时间,配置获取连接等待超时,时间单位都是毫秒ms
    maxWait: 60000
    #配置间隔多久才进行一次检测,检测需要关闭的空闲连接
    timeBetweenEvictionRunsMillis: 60000
    #配置一个连接在池中最小生存的时间
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

然后在model中加入订单和商品的 pojo

@Data
@Entity
@Table(name = "seckill_order")
public class SeckillOrder {
    //订单ID
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //支付金额
    private BigDecimal money;
    //秒杀用户的用户ID
    private Long userId;
    //创建时间
    private Date createTime;
    //支付时间
    private Date payTime;
    //秒杀商品,和订单是一对多的关系
    private Long goodId;
    //订单状态, -1:无效 0:成功 1:已付款
    private Integer orderStatus ;
}

@Entity
@Table(name = "seckill_good")
public class SeckillGood {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //商品标题
    private String title;
    //商品照片
    private String image;
    //商品原价格
    private BigDecimal price;
    //商品秒杀价格
    private BigDecimal costPrice;
    //创建时间
    private Date createTime;
    //秒杀开始时间
    private Date startTime;
    //秒杀结束时间
    private Date endTime;
    //剩余库存数量
    private long stockCount;
}

在数据库中分别建立这两张表,sql我附在git的文件中了。这里的订单表的id应该由分布式的雪花id生成器生成,由于时间问题这里直接使用自增id。 使用jpa支持增删改查。

public interface SeckillGoodRepository extends JpaRepository<SeckillGood, Long>,
        JpaSpecificationExecutor<SeckillGood> {
}

分布式锁

主流的分布式锁实现方案有两种:

  • 基于Redis的分布式锁。使用并发量很大、性能要求很高而可靠性问题可以通过其他方案弥补的场景
  • 基于ZooKeeper的分布式锁。适用于高可靠(高可用),而并发量不是太高的场景、 这里我们主要是用于秒杀场景的话,redis实现的分布式锁就更合适了。 首先我们引入pom配置
<redisson-version>3.6.5</redisson-version>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>${redisson.version}</version>
</dependency>

然后在配置中加入配置类

@Configuration
public class RedissonConfig {

    @Value("${redisson.address}")
    private String addressUrl; 

    @Bean
    public RedissonClient getRedisson() throws Exception{
        RedissonClient redisson = null;
        Config config = new Config();
        config.useSingleServer()
                .setAddress(addressUrl);
        redisson = Redisson.create(config);

        System.out.println(redisson.getConfig().toJSON().toString());
        return redisson;
    }
}

编写redis锁的实现类:

@Component
public class RedissonLockerImpl implements RedissonLocker {

    @Autowired
    private RedissonClient redissonClient; 

    @Override
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    @Override
    public RLock lock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
        return null;
    }

    @Override
    public RLock lock(String lockKey, TimeUnit unit, long timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

编写一个简单的方法测试分布式锁

@Component
public class RedissionOrderServiceImpl implements RedissionOrderService {

    private static final String LOCK_KEY = "seckill_key";

    @Autowired
    SeckillGoodService seckillGoodService;

    @Autowired
    SeckillOrderService seckillOrderService;

    @Autowired
    private RedissonLocker redissonLocker;

    /**
     * 下单步骤:校验库存,扣库存,创建订单,支付
     *
     */
    @Override
    public Boolean saveOrder(Long sid) {
        try {
            redissonLocker.lock(LOCK_KEY);
            /**
             * 1.查库存
             */
            SeckillGoodDTO goodDTO = seckillGoodService.findById(sid);
            if (goodDTO == null || goodDTO.getStockCount() <= 0 ) {
                throw new RuntimeException("库存不足");
            }

            /**
             * 2.根据查询出来的库存,更新已卖库存数量
             */
            int count = seckillGoodService.updateGoodStock(sid);
            if (count == 0){
                throw new RuntimeException("库存为0");
            }

            /**
             * 3.创建订单
             */
            SeckillOrderDTO order = new SeckillOrderDTO();
            order.setGoodId(goodDTO.getId());
            int id = seckillOrderService.save(order);
            if (id > 0) {
                return true;
            }
            return false;

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            redissonLocker.unlock(LOCK_KEY); // 释放锁
        }

        return false;
    }
}

接口限流

在zuul我们可以通过网关过滤器实现限流,秒杀系统可以同时使用两种维度的限流,第一种是针对用户纬度的限流,限制用户发起的请求数量,在用户发起以后禁止用户再度抢购。第二种是针对商品纬度的限流,限制对商品发起的请求数量,在请求数量在时间段内达到限制以后禁止前台接口再度发起请求,用来防止后台的秒杀服务出现雪崩。

在这里我们利用zuul的过滤器和redis锁来实现一个商品维度的过滤器,通过对前端秒杀的请求接口进行拦截,然后利用redis分布式锁计数器对当前秒杀的数量进行判断。 首先我们在zuul服务的pom中加入redis依赖

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

在yml中加入配置

redis:
  database: 0
  port: 6379
  password:
  timeout: 3000
  host: 127.0.0.1
  #    cluster:
  #      nodes: 192.168.234.18:6579,192.168.234.28:6579,192.168.234.29:6579,192.168.234.30:6579,192.168.234.6:6579,192.168.234.43:6579
  pool:
    max-active: 8
    max-wait: 3000
    max-idle: 8
    min-idle: 0

加入redis相关的配置以确保json格式的序列化,

@Configuration
public class BeanConfiguration {

    @Value("${spring.redis.host}")
    private String hostName;

    @Value("${spring.redis.port}")
    private Integer port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(hostName);
        configuration.setPort(port);
        configuration.setPassword(password);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 设置值(value)的序列化采用Jackson2JsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // 设置键(key)的序列化采用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }

编写zuul过滤器:

@Component
public class SeckillLimitFilter extends ZuulFilter {
    private static final Logger LOGGER = Logger.getLogger(SeckillLimitFilter.class.getName());

    /**
     * Redis限流服务实例
     */
    @Resource
    SeckillLimitService seckillLimitService;

    @Override
    public String filterType() {
        return "pre"; //路由之前
    }
 
    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        /**
         *如果请求已经被其他的过滤器终止,本过滤器就不做处理
         **/
        if (!ctx.sendZuulResponse()) {
            return false;
        }
        /**
         *对
         */
        LOGGER.info("*******request: " + request.getRequestURI());
        if (request.getRequestURI().startsWith
                ("/seckill-provider/api/seckill/do")) {
            return true;
        }
        return false;
    }
    /**
     * 过滤器
     */
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Map<String, Object> param= ZuulParameterUtil.getRequestParams(ctx);
        LOGGER.info("*******run: "+param);
        String goodId = String.valueOf(param.get("goodId"));

        if (goodId != null) {
          String cacheKey = "seckill:" + goodId;
          Boolean limited = seckillLimitService.trySeckill(cacheKey,100L);
            if (!limited) {
                 /**
                 *降级处理
                 */
                String msg = "参与抢购的人太多,请稍后再试";
                fallback(ctx, msg);
                return null;
            }
            return null;
        } else {
            /**
             *参数输入错误
             */
            String msg = "参数错误";
            fallback(ctx, msg);
            return null;
        }
    }
    /**
     * 限流后的降级处理
     * @param ctx
     * @param msg
     */
    private void fallback(RequestContext ctx, String msg) {
        ctx.setSendZuulResponse(false);
        try {
            ctx.getResponse().setContentType("text/html;charset=utf-8");
            ctx.getResponse().getWriter().write(msg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

安装jmeter来测试一下接口。

屏幕快照 2022-04-29 下午9.49.26.png 简单测一下一百人抢购。

一下子就把我的系统打崩了呀

屏幕快照 2022-04-29 下午10.01.15.png 查看一下原因,Zuul网关调用的队列只有10个,如果超过10个排队就出现问题了。