关于linux:秒杀系统后台实现详解

9次阅读

共计 15105 个字符,预计需要花费 38 分钟才能阅读完成。

秒杀后盾实现

本文次要解说我的项目实战中秒杀如何解决上面问题:

1)实现秒杀异步下单, 把握如何保障生产者 & 消费者音讯不失落

2)实现避免歹意刷单

3)实现避免雷同商品反复秒杀

4)实现秒杀下单接口暗藏

5)实现下单接口限流

1 秒杀异步下单

用户在下单的时候,须要基于 JWT 令牌信息进行登陆人信息认证,确定以后订单是属于谁的。

针对秒杀的非凡业务场景,仅仅依附对象缓存或者页面动态化等技术去解决服务端压力还是远远不够。

对于数据库压力还是很大,所以须要异步下单,异步是最好的解决办法,但会带来一些额定的程序上的

复杂性。

1.1 秒杀服务 - 下单实现
1)将 tokenDecode 工具类 config 放入秒杀服务并申明 Bean

public static void main(String[] args){SpringApplication.run(SeckillApplication,class,args);
}
@Bean
public TokenDecode tokenDecode(){return new TokenDecode();
}
2)更新秒杀服务启动类,增加 redis 配置
/**
    * 设置 redisTemplate 的序列化设置 
    * @param redisConnectionFactory 
    * @return 
    */ 
@Bean 
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { 
    // 1. 创立 redisTemplate 模版 
    RedisTemplate<Object, Object> template = new RedisTemplate<>(); 
    // 2. 关联 redisConnectionFactory
     template.setConnectionFactory(redisConnectionFactory);
    // 3. 创立 序列化类 
    GenericToStringSerializer genericToStringSerializer = new     GenericToStringSerializer(Object.class); 
    // 4. 序列化类,对象映射设置 
    // 5. 设置 value 的转化格局和 key 的转化格局
    template.setValueSerializer(genericToStringSerializer);
    template.setKeySerializer(new StringRedisSerializer()); 
    template.afterPropertiesSet(); 
    return template; 
}
2)新建下单 controller 并申明办法
@RestController 
@CrossOrigin 
@RequestMapping("/seckillorder") 
public class SecKillOrderController { 
    @Autowired 
    private TokenDecode tokenDecode; 
    @Autowired 
    private SecKillOrderService secKillOrderService; 
    /**
    * 秒杀下单 
    * @param time 以后时间段 
    * @param id 秒杀商品 id 
    * @return 
    */ 
    @RequestMapping("/add")
    // 获取以后登陆人 
    String username = tokenDecode.getUserInfo().get("username"); 
    boolean result = secKillOrderService.add(id,time,username); 
    if (result){return new Result(true, StatusCode.OK,"下单胜利"); 
    }else{return new Result(false,StatusCode.ERROR,"下单失败"); 
        } 
    } 
}
3) 新建 service 接口
public interface SecKillOrderService { 
/**
* 秒杀下单 
* @param id 商品 id 
* @param time 时间段 
* @param username 登陆人姓名 
* @return 
*/ 
boolean add(Long id, String time, String username); 
} 
4)更改预加载秒杀商品

当预加载秒杀商品的时候,提前加载每一个商品的库存信息,后续减库存操作也会先预扣减缓存中的库存再异步扣减 mysql 数据。

预扣减库存会基于 redis 原子性操作实现

for (SeckillGoods seckillGoods : seckillGoodsList) {redisTemplate.boundHashOps(SECKILL_GOODS_KEY +     redisExtName).put(seckillGoods.getId(),seckillGoods); 
        // 预加载库存信息 
redisTemplate.OpsForValue(SECKILL_GOODS_STOCK_COUNT_KEY+seckillGoods.getId(),se 
ckillGoods.getStockCount()); 
} 
6)秒杀下单业务层实现

业务逻辑:

获取秒杀商品数据与库存量数据,如果没有库存则抛出异样执行 redis 预扣减库存, 并获取扣减之后的库存值如果扣减完的库存值 <=0, 则删除 redis 中对应的商品信息与库存信息基于 mq 异步形式实现与 mysql 数据同步(最终一致性)

留神:库存数据从 redis 中取出,转换成 String

@Service 
public class SecKillOrderServiceImpl implements SecKillOrderService { 
    @Autowired 
    private RedisTemplate redisTemplate; 
    @Autowired 
    private IdWorker idWorker; 
    @Autowired 
    private CustomMessageSender customMessageSender; 
    /**
    * 秒杀下单 
    * @param id 商品 id 
    * @param time 时间段 
    * @param username 登陆人姓名 
    * @return 
    */ 
    @Override 
    public boolean add(Long id, String time, String username) { 
    // 获取商品数据 
    SeckillGoods goods = (SeckillGoods) 
redisTemplate.boundHashOps("SeckillGoods_" + time).get(id); 
    String redisStock = (String) redisTemplate.boundValueOps("StockCount_" + 
goods.getId()).get(); 
    if(StringUtils.isEmpty(redisStock)){return false;}
    int value=Integer.parseInt(redisStock); 
    // 如果没有库存,则间接抛出异样 
    if(goods==null || value<=0){return false;}
    //redis 预扣库存 
    Long stockCount = redisTemplate.boundValueOps("StockCount_" + 
id).decrement(); 
    if (stockCount<=0){ 
    // 库存没了 
    // 删除商品信息 
    redisTemplate.boundHashOps("SeckillGoods_" + time).delete(id); 
    // 删除对应的库存信息 
    redisTemplate.delete("StockCount_" + goods.getId()); 
}
    // 有库存 
    // 如果有库存,则创立秒杀商品订单 
    SeckillOrder seckillOrder = new SeckillOrder(); 
    seckillOrder.setId(idWorker.nextId());
    seckillOrder.setUserId(username);     
    seckillOrder.setSellerId(goods.getSellerId());
    seckillOrder.setCreateTime(new Date()); 
    seckillOrder.setStatus("0"); 
    // 发送音讯 
    return true; 
    } 
 }

1.2 生产者保障音讯不失落

依照现有 rabbitMQ 的相干常识,生产者会发送音讯达到音讯服务器。然而在理论生产环境下,音讯生产者发送的音讯很有可能当达到了音讯服务器之后,因为音讯服务器的问题导致音讯失落,如宕机。因为音讯服务器默认会将音讯存储在内存中。一旦音讯服务器宕机,则音讯会产生失落。因而要保障生产者的音讯不失落,要开始长久化策略。

rabbitMQ 长久化:交换机长久化 队列长久化 音讯长久化

然而如果仅仅只是开启这两局部的长久化,也很有可能造成音讯失落。因为音讯服务器很有可能在长久化的过程中呈现宕机。因而须要通过数据保护机制来保障音讯肯定会胜利进行长久化,否则将始终进行音讯发送。

事务机制 
    事务机制采纳类数据库的事务机制进行数据保护,当音讯达到音讯服务器,首先会开启一个事务,接着进 行数据磁盘长久化,只有长久化胜利才会进行事务提交,向音讯生产者返回胜利告诉,音讯生产者一旦接管成 功告诉则不会再发送此条音讯。当出现异常,则返回失败告诉. 音讯生产者一旦接管失败告诉,则持续发送该 条音讯。事务机制尽管可能保障数据安全,然而此机制采纳的是同步机制,会产生零碎间音讯阻塞,影响整个零碎 的音讯吞吐量。从而导致整个零碎的性能降落,因而不倡议应用。confirm 机制 
        confirm 模式须要基于 channel 进行设置, 一旦某条音讯被投递到队列之后, 音讯队列就会发送一个确 认信息给生产者, 如果队列与音讯是可长久化的, 那么确认音讯会等到音讯胜利写入到磁盘之后收回.             confirm 的性能高, 次要得益于它是异步的. 生产者在将第一条音讯收回之后期待确认音讯的同时也能够 持续发送后续的音讯. 当确认音讯达到之后, 就能够通过回调办法解决这条确认音讯. 如果 MQ 服务宕机了, 则会 返回 nack 音讯. 生产者同样在回调办法中进行后续解决。
1.2.1 开启 confifirm 机制
1)更改秒杀服务配置文件
rabbitmq: 
    host: 192.168.200.128 
    publisher-confirms: true #开启 confirm 机制
2)开启队列长久化
@Configuration 
public class RabbitMQConfig { 
    // 秒杀商品订单音讯 
    public static final String SECKILL_ORDER_KEY="seckill_order"; 
    @Bean 
    public Queue queue(){ 
        // 开启队列长久化 
        return new Queue(SECKILL_ORDER_KEY,true); 
        } 
    }
3)音讯长久化源码查看

4)加强 rabbitTemplate
@Component 
public class CustomMessageSender implements RabbitTemplate.ConfirmCallback {static final Logger log = LoggerFactory.getLogger(CustomMessageSender.class); 
    private static final String MESSAGE_CONFIRM="message_confirm"; 
    @Autowired 
    private RabbitTemplate rabbitTemplate; 
    @Autowired 
    private RedisTemplate redisTemplate; 
    public CustomMessageSender(RabbitTemplate rabbitTemplate) { 
    this.rabbitTemplate = rabbitTemplate; 
    rabbitTemplate.setConfirmCallback(this); 
}
    @Override 
    public void confirm(CorrelationData correlationData, boolean ack, String 
cause) {if (ack){ 
    // 返回胜利告诉 
    // 删除 redis 中的相干数据 
    redisTemplate.delete(correlationData.getId()); 
    redisTemplate.delete(MESSAGE_CONFIRM_+correlationData.getId()); 
    }else{
    // 返回失败告诉 
    Map<String,String> map = 
(Map<String,String>)redisTemplate.opsForHash().entries(MESSAGE_CONFIRM_+correlationData.getId()); 
    String exchange = map.get("exchange"); 
    String routingKey = map.get("routingKey"); 
    String sendMessage = map.get("sendMessage"); 
    // 从新发送 
    rabbitTemplate.convertAndSend(exchange,routingKey, 
JSON.toJSONString(sendMessage)); 
    } 
}
    // 自定义发送办法 
    public void sendMessage(String exchange,String routingKey,String message){ 
    // 设置音讯惟一标识并存入缓存 
    CorrelationData correlationData = new 
CorrelationData(UUID.randomUUID().toString()); 
    redisTemplate.opsForValue().set(correlationData.getId(),message);
    Map<String, String> map = new HashMap<>(); 
    map.put("exchange", exchange); 
    map.put("routingKey", routingKey); 
    map.put("sendMessage", message); 
redisTemplate.opsForHash().putAll(MESSAGE_CONFIRM_+correlationData.getId(),map) 
; 
// 携带惟一标识发送音讯 
rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData); 
    } 
}
5)发送音讯

更改下单业务层实现

@Autowired 
private CustomMessageSender customMessageSender;

1.3 秒杀下单服务更新库存

1.3.1 异步下单服务 service_consume
1)增加依赖
<dependencies> 
<dependency> 
<groupId>com.changgou</groupId> 
<artifactId>changgou_common_db</artifactId> 
<version>1.0-SNAPSHOT</version> 
</dependency> 
<dependency> 
<groupId>org.springframework.cloud</groupId> 
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
</dependency> 
<dependency> 
<groupId>com.changgou</groupId> 
<artifactId>changgou_service_order_api</artifactId> 
<version>1.0-SNAPSHOT</version> 
</dependency> 
<dependency> 
<groupId>com.changgou</groupId> 
<artifactId>changgou_service_seckill_api</artifactId> 
<version>1.0-SNAPSHOT</version> 
</dependency> 
<dependency> 
<groupId>com.changgou</groupId> 
<artifactId>changgou_service_goods_api</artifactId> 
<version>1.0-SNAPSHOT</version>
</dependency> 
<dependency> 
<groupId>org.springframework.amqp</groupId> 
<artifactId>spring-rabbit</artifactId> 
</dependency> 
</dependencies> 
2)新建 application.yml
server: 
    port: 9022 
spring: 
    jackson: 
        time-zone: GMT+8 
    application: 
        name: sec-consume 
    datasource: 
        driver-class-name: com.mysql.jdbc.Driver 
        url: jdbc:mysql://192.168.200.128:3306/changgou_seckill? 
useUnicode=true&characterEncoding=utf- 
8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8 
        username: root 
        password: root 
    main: 
        allow-bean-definition-overriding: true #当遇到同样名字的时候,是否容许笼罩注册 
    redis: 
        host: 192.168.200.128 
    rabbitmq: 
        host: 192.168.200.128 
    eureka: 
        client: 
            service-url: 
                defaultZone: http://127.0.0.1:6868/eureka 
        instance: 
            prefer-ip-address: true 
feign: 
    hystrix: 
        enabled: true 
    client: 
        config: 
            default: #配置全局的 feign 的调用超时工夫 如果 有指定的服务配置 默认的配置不会失效 
            connectTimeout: 60000 # 指定的是 消费者 连贯服务提供者的连贯超时工夫 是否能连贯 
单位是毫秒
            readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时工夫()单位是毫秒 
#hystrix 配置 
hystrix: 
    command: 
        default: 
            execution: 
                timeout: 
                #如果 enabled 设置为 false,则申请超时交给 ribbon 管制 
                enabled: true 
                isolation: 
                    strategy: SEMAPHORE 
                    thread: 
                    # 熔断器超时工夫,默认:1000/ 毫秒 
                        timeoutInMilliseconds: 20000
3)新建启动类
@SpringBootApplication 
@EnableDiscoveryClient 
@MapperScan(basePackages = {"com.changgou.consume.dao"}) 
public class OrderConsumerApplication {public static void main(String[] args) {SpringApplication.run(OrderConsumerApplication.class,args); 
    } 
}
1.3.2 消费者手动 ACK 下单实现

依照现有 RabbitMQ 常识,能够得悉当音讯消费者胜利接管到音讯后,会进行生产并主动告诉音讯服务器将该条音讯删除。此种形式的实现应用的是消费者自动应答机制。然而此种形式十分的不平安。在生产环境下,当音讯消费者接管到音讯,很有可能在解决音讯的过程中出现意外状况从而导致音讯失落,因为如果应用自动应答机制是十分不平安。咱们须要确保消费者当把音讯胜利解决实现之后,音讯服务器才会将该条音讯删除。此时要实现这种成果的话,就须要将 自动应答转换为手动应答, 只有在音讯消费者将音讯解决完,才会告诉音讯服务器将该条音讯删除。

1)更改配置文件
rabbitmq: 
    host: 192.168.200.128 
    listener: 
        simple: 
            acknowledge-mode: manual #手动
2)定义监听类
@Component 
public class ConsumeListener { 
    @Autowired 
    private SecKillOrderService secKillOrderService; 
    @RabbitListener(queues = RabbitMQConfig.SECKILL_ORDER_KEY) 
    public void receiveSecKillOrderMessage(Channel channel, Message message){ 
    // 转换音讯 
    SeckillOrder seckillOrder = JSON.parseObject(message.getBody(), 
SeckillOrder.class); 
    // 同步 mysql 订单 
    int rows = secKillOrderService.createOrder(seckillOrder); 
    if (rows>0){ 
        // 返回胜利告诉 
        try {channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); 
        } catch (IOException e) {e.printStackTrace();
    } 
    }else{
    // 返回失败告诉 
    try {
    // 第一个 boolean true 所有消费者都会回绝这个音讯,false 代表只有以后消费者拒 
绝 
    // 第二个 boolean true 以后音讯会进入到死信队列,false 从新回到原有队列中,默
认回到头部 
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false); 
    } catch (IOException e) {e.printStackTrace(); 
            } 
        } 
    } 
} 

3)定义业务层接口与实现类

public interface ConsumeService {int handleCreateOrder(SeckillOrder order); 
}
@Service
public class SecKillOrderServiceImpl implements SecKillOrderService {              @Autowired 
    private SeckillGoodsMapper seckillGoodsMapper; 
    @Autowired 
    private SeckillOrderMapper seckillOrderMapper; 
    /**
    * 增加订单 
    * @param seckillOrder 
    * @return 
    */ 
    @Override 
    @Transactional 
    public int createOrder(SeckillOrder seckillOrder) {int result =seckillGoodsMapper.updateStockCount(seckillOrder.getSeckillId()); 
    if (result<=0){return result;}
    result =seckillOrderMapper.insertSelective(seckillOrder);
    if (result<=0){return result;}return 1;
数据库字段 unsigned 介绍 
unsigned----- 无符号,润饰 int、char 
ALTER TABLE tb_seckill_goods MODIFY COLUMN stock_count int(11) UNSIGNED DEFAULT NULL COMMENT '残余库存数';

1.5 流量削峰

在秒杀这种高并发的场景下,每秒都有可能产生几万甚至十几万条音讯,如果没有对音讯处理量进行任何限度的话,很有可能因为过多的音讯沉积从而导致消费者宕机的状况。因而官网倡议对每一个音讯消费者都设置解决音讯总数(音讯抓取总数)。

音讯抓取总数的值,设置过大或者过小都不好,过小的话,会导致整个零碎音讯吞吐能力降落,造成性能节约。过大的话,则很有可能导致音讯过多,导致整个零碎 OOM。因而官网倡议每一个消费者将该值设置在 100-300 之间。

1)更新消费者。

// 设置预抓取总数 
channel.basicQos(300);

1.6 秒杀渲染服务 - 下单实现

1)定义 feign 接口
@FeignClient(name="seckill") 
public interface SecKillOrderFeign { 
    /**
     * 秒杀下单 
     * @param time 以后时间段 
     * @param id 秒杀商品 id 
     * @return 
     */ 
     @RequestMapping("/seckillorder/add") 
     public Result add(@RequestParam("time") String time, @RequestParam("id") Long id); 
    }
2)定义 controller
@Controller 
@CrossOrigin 
@RequestMapping("/wseckillorder")
public class SecKillOrderController { 
    @Autowired 
    private SecKillOrderFeign secKillOrderFeign; 
    /**
        * 秒杀下单 
        * @param time 以后时间段 
        * @param id 秒杀商品 id 
        * @return 
        */ 
        @RequestMapping("/add") 
        @ResponseBody 
        public Result add(String time,Long id){Result result = secKillOrderFeign.add(time, id); 
        return result; 
        }
    }

2 避免歹意刷单解决

在生产场景下,很有可能会存在某些用户歹意刷单的状况呈现。这样的操作对于零碎而言,会导致业务出错、脏数据、后端拜访压力大等问题的呈现。

个别要解决这个问题的话,须要前端进行管制,同时后端也须要进行管制。后端实现能够通过 Redisincrde 原子性递增来进行解决。

2.1 更新秒杀服务下单

2.2 防重办法实现
// 避免反复提交 
private String preventRepeatCommit(String username,Long id) { 
    String redisKey = "seckill_user_" + username+"_id_"+id; 
    long count = redisTemplate.opsForValue().increment(redisKey, 1); 
    if (count == 1){ 
        // 设置有效期五分钟 
        redisTemplate.expire(redisKey, 5, TimeUnit.MINUTES); 
        return "success"; 
        }
        if (count>1){return "fail";}
            return "fail";
            }

3 避免雷同商品反复秒杀

3.1 批改下单业务层实现

3.2 dao 层新增查询方法
public interface SeckillOrderMapper extends Mapper<SeckillOrder> { 
    /**
     * 查问秒杀订单信息 
     * @param username 
     * @param id 
     * @return 
     */ 
     @Select("select * from tb_seckill_order where user_id=#{username} and seckill_id=#{id}") 
     SeckillOrder getSecKillOrderByUserNameAndGoodsId(String username, Long id); }

4 秒杀下单接口暗藏

以后尽管能够确保用户只有在登录的状况下才能够进行秒杀下单,然而无奈办法有一些歹意的用户在登录了之后,猜想秒杀下单的接口地址进行歹意刷单。所以须要对秒杀接口地址进行暗藏。

在用户每一次点击抢购的时候,都首先去生成一个随机数并存入 redis,接着用户携带着这个随机数去拜访秒杀下单,下单接口首先会从 redis 中获取该随机数进行匹配,如果匹配胜利,则进行后续下单操作,如果匹配不胜利,则认定为非法拜访。

4.1 将随机数工具类放入 common 工程中
public class RandomUtil {public static String getRandomString() { 
    int length = 15; 
    String base = "abcdefghijklmnopqrstuvwxyz0123456789"; 
    Random random = new Random(); 
    StringBuffer sb = new StringBuffer(); 
    for (int i = 0; i < length; i++) {int number = random.nextInt(base.length()); 
        sb.append(base.charAt(number)); 
}
    return sb.toString();}
public static void main(String[] args) {String randomString = RandomUtil.getRandomString();
}
4.2秒杀渲染服务定义随机数接口
/** 
* 接口加密 
* 生成随机数存入 redis,10 秒有效期 
*/
@GetMapping("/getToken") 
@ResponseBody
public String getToken(){String randomString = RandomUtil.getRandomString(); 
    String cookieValue = this.readCookie(); 
    redisTemplate.boundValueOps("randomcode_"+cookieValue).set(randomString,10, TimeUnit.SECONDS);
    return randomString; 
    }
// 读取 cookie private String readCookie(){HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 
    String cookieValue = CookieUtil.readCookie(request, "uid").get("uid");      
    return cookieValue; 
}
4.3 js 批改

批改 js 下单办法

// 秒杀下单 
add:function(id){
    app.msg ='正在下单'; 
    // 获取随机数 
    axios.get("/api/wseckillorder/getToken").then(function (response) { 
    var random=response.data; 
    axios.get("/api/wseckillorder/add? time="+moment(app.dateMenus[0]).format("YYYYMMDDHH")+"&id="+id+"&random="+random ).then(function (response) {if (response.data.flag){app.msg='抢单胜利,行将进入领取!';}else{app.msg='抢单失败';}
    })
 }) 
}

4.4 秒杀渲染服务更改

批改秒杀渲染服务下单接口

/** 
 * 秒杀下单 
 * @param time 以后时间段 
 * @param id 秒杀商品 id 
 * @return 
 */ 
 @RequestMapping("/add") 
 @ResponseBody 
 public Result add(String time,Long id,String random){ 
 // 校验密文无效 
 String randomcode = (String) redisTemplate.boundValueOps("randomcode").get();     if (StringUtils.isEmpty(randomcode) || !random.equals(randomcode)){return new Result(false, StatusCode.ERROR,"有效拜访"); 
 }
     Result result = secKillOrderFeign.add(time, id);
    return result; 
   }

5 秒杀下单接口限流

因为秒杀的非凡业务场景,生产场景下,还有可能要对秒杀下单接口进行拜访流量管制,避免过多的申请进入到后端服务器。对于限流的实现形式,咱们之前曾经接触过通过 nginx 限流,网关限流。然而他们都是对一个大的服务进行拜访限流,如果当初只是要对某一个服务中的接口办法进行限流呢?这里举荐应用 google 提供的 guava 工具包中的 RateLimiter 进行实现,其外部是基于令牌桶算法进行限流计算

1)增加依赖
<dependency> 
    <groupId>com.google.guava</groupId> 
    <artifactId>guava</artifactId> 
    <version>28.0-jre</version> 
</dependency>
2)自定义限流注解
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {}
3)自定义切面类
@Component 
@Scope 
@Aspect 
public class AccessLimitAop { 
    @Autowired 
    private HttpServletResponse httpServletResponse; 
    private RateLimiter rateLimiter = RateLimiter.create(20.0);                   @Pointcut("@annotation(com.changgou.webSecKill.aspect.AccessLimit)") 
    public void limit(){} @Around("limit()") 
    public Object around(ProceedingJoinPoint proceedingJoinPoint){boolean flag = rateLimiter.tryAcquire(); 
    Object obj = null; 
    try{if (flag){obj=proceedingJoinPoint.proceed(); 
        }else{String errorMessage = JSON.toJSONString(new Result(false,StatusCode.ERROR,"fail"));               
        outMessage(httpServletResponse,errorMessage); 
        } 
        }catch (Throwable throwable) {throwable.printStackTrace(); 
        }return obj; 
        }
private void outMessage(HttpServletResponse response, String errorMessage) {     ServletOutputStream outputStream = null; 
    try {response.setContentType("application/json;charset=UTF-8");
    outputStream = response.getOutputStream();
    outputStream.write(errorMessage.getBytes("UTF-8")); 
    } catch (IOException e) {e.printStackTrace(); 
    }finally {try {outputStream.close(); 
    } catch (IOException e) {e.printStackTrace();
    }
  }
4)应用自定义限流注解

欢送观看并写出本人的见解!独特探讨!


最初,最近很多小伙伴找我要Linux 学习路线图,于是我依据本人的教训,利用业余时间熬夜肝了一个月,整顿了一份电子书。无论你是面试还是自我晋升,置信都会对你有帮忙!

收费送给大家,只求大家金指给我点个赞!

电子书 | Linux 开发学习路线图

也心愿有小伙伴能退出我,把这份电子书做得更完满!

有播种?心愿老铁们来个三连击,给更多的人看到这篇文章

举荐浏览:

  • 干货 | 程序员进阶架构师必备资源免费送
  • 神器 | 反对搜寻的资源网站

正文完
 0