一起养成写作习惯!这是我参与「掘金日新计划 · 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可以看到服务全部注册成功
我们有一个网关服务zuul,一个注册中心eureka,2个服务提供者eurekaservice
我们在两个提供的服务的eurekaservice都配置了相同的应用名称
spring:
application:
name: eureka-client
根据路由规则就会轮流访问 刚刚部署的两个服务,这样负载均衡就搭建完了。
数据库层面
因为此次我们只是简单的设计一个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来测试一下接口。
简单测一下一百人抢购。
一下子就把我的系统打崩了呀
查看一下原因,Zuul网关调用的队列只有10个,如果超过10个排队就出现问题了。