zoukankan      html  css  js  c++  java
  • SpringBoot+Mysql+Redis+RabbitMQ+Jmeter模拟实现高并发秒杀

    文章前言

    众所周知,当遇到比较多数据不一致的问题时,大多数都是因为并发请求时,没及时处理的原因,提一个电商平台比较经常出现得高并发场景限时秒杀活动,他们是怎么来防止超卖呢?如何实现高并发秒杀呢?。
    本文模拟了高并发秒杀,并且防止了超卖,也模拟了纯数据库秒杀超卖得场景,本次模拟demo得框架技术为:SpringBoot+Mysql+Redis+RabbitMQ+tkmybatis

    数据库表结构:

     

     

     一个为库存表,一个为订单表,本人使用得是mysql8.0。

    完整得项目工具展示

    Jmeter :

    redisManager :

    RabbitMQ :

    编写代码

    1.首先新建Springboot项目

    2.可以先不勾选需要得jar包,项目初始化好之后,使用maven导入项目需要得jar包
    pom.xml :

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintage</groupId>
                        <artifactId>junit-vintage-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.8.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-amqp</artifactId>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.7.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
            </dependency>
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>2.0.3-beta1</version>
            </dependency>
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper</artifactId>
                <version>4.0.0</version>
            </dependency>
        </dependencies>
    

    3.配置application.properties

    spring.devtools.restart.enabled=false
    ##配置数据库连接
    spring.datasource.username=root 
    spring.datasource.password=root
    server.port=8443
    spring.datasource.url=jdbc:mysql://localhost:3306/ktoa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    ##配置rabbitmq连接
    spring.rabbitmq.host=localhost
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=guest
    spring.rabbitmq.password=guest
    
    ##配置连接redis --都记得打开服务
    spring.redis.host=localhost
    spring.redis.port=6379 
    spring.redis.jedis.pool.max-active=1024
    spring.redis.jedis.pool.max-wait=-1s
    spring.redis.jedis.pool.max-idle=200
    spring.redis.password=123456
    

     这时可以启动一下springboot项目是否能够正常启动,如没问题可以继续往下编写!!
    4.新建pojo包,添加实体类

    Order.java:

    import lombok.Data;
    import javax.persistence.Column;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import java.io.Serializable;
    @Data
    @Table(name = "t_order")
    public class Order implements Serializable {
        private static final long serialVersionUID = -8867272732777764701L;
    
        @Id
        @Column(name = "id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(name = "order_name")
        private String order_name;
    
        @Column(name = "order_user")
        private String order_user;
    }
    

     Stock.java:

    import lombok.Data;
    import javax.persistence.Column;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import java.io.Serializable;
    @Table(name = "stock")
    @Data
    public class Stock implements Serializable {
        private static final long serialVersionUID = 2451194410162873075L;
    
        @Id
        @Column(name = "id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(name = "name")
        private String name;
    
        @Column(name = "stock")
        private Long stock;
    }
    

     因为本次数据库操作方面使用了tkmybatis框架,所以实体类我们需要用到JPA的注解,来实现映射关系!!

     5.配置tkmybatis得接口
    新建名为base得包,在base下面新建service得接口

     GenericMapper.interface:

    import tk.mybatis.mapper.common.Mapper;
    import tk.mybatis.mapper.common.MySqlMapper;
    public interface GenericMapper<T> extends Mapper<T>, MySqlMapper<T> {
    }
    

    关于这个接口得作用你需要了解太多,你只要知道我们得mapper层需要通过继承它来实现数据库操作,如果你接触过jpa或者mybatis-plus,tkmybatis方式跟它们相似。

    6.新建mapper层
    新建名为mapper得包,在这个包下面新建

     OrderMapper.interface:

    import com.spbtrediskill.secondskill.base.service.GenericMapper;
    import com.spbtrediskill.secondskill.pojo.Order;
    import org.apache.ibatis.annotations.Mapper;
    @Mapper
    public interface OrderMapper extends GenericMapper<Order> {
        void insertOrder(Order order);
    }
    

    StockMapper.interface:

    import com.spbtrediskill.secondskill.base.service.GenericMapper;
    import com.spbtrediskill.secondskill.pojo.Stock;
    import org.apache.ibatis.annotations.Mapper;
    @Mapper
    public interface StockMapper extends GenericMapper<Stock> {
    }
    

    7.编写RabbitMQ和redis得配置类

    新建config包,新建redis和RabbitMQ得类
    在这里插入图片描述

    MyRabbitMQConfig.java:

    import org.springframework.amqp.core.Binding;
    import org.springframework.amqp.core.BindingBuilder;
    import org.springframework.amqp.core.Exchange;
    import org.springframework.amqp.core.ExchangeBuilder;
    import org.springframework.amqp.core.Queue;
    import org.springframework.amqp.core.QueueBuilder;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.util.HashMap;
    import java.util.Map;
    @Configuration
    public class MyRabbitMQConfig {
    
        //库存交换机
        public static final String STORY_EXCHANGE = "STORY_EXCHANGE";
    
        //订单交换机
        public static final String ORDER_EXCHANGE = "ORDER_EXCHANGE";
    
        //库存队列
        public static final String STORY_QUEUE = "STORY_QUEUE";
    
        //订单队列
        public static final String ORDER_QUEUE = "ORDER_QUEUE";
    
        //库存路由键
        public static final String STORY_ROUTING_KEY = "STORY_ROUTING_KEY";
    
        //订单路由键
        public static final String ORDER_ROUTING_KEY = "ORDER_ROUTING_KEY";
        @Bean
        public MessageConverter messageConverter() {
            return new Jackson2JsonMessageConverter();
        }
        //创建库存交换机
        @Bean
        public Exchange getStoryExchange() {
            return ExchangeBuilder.directExchange(STORY_EXCHANGE).durable(true).build();
        }
        //创建库存队列
        @Bean
        public Queue getStoryQueue() {
            return new Queue(STORY_QUEUE);
        }
        //库存交换机和库存队列绑定
        @Bean
        public Binding bindStory() {
            return BindingBuilder.bind(getStoryQueue()).to(getStoryExchange()).with(STORY_ROUTING_KEY).noargs();
        }
        //创建订单队列
        @Bean
        public Queue getOrderQueue() {
            return new Queue(ORDER_QUEUE);
        }
        //创建订单交换机
        @Bean
        public Exchange getOrderExchange() {
            return ExchangeBuilder.directExchange(ORDER_EXCHANGE).durable(true).build();
        }
        //订单队列与订单交换机进行绑定
        @Bean
        public Binding bindOrder() {
            return BindingBuilder.bind(getOrderQueue()).to(getOrderExchange()).with(ORDER_ROUTING_KEY).noargs();
        }
    }

    RedisConfig .java:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    public class RedisConfig {
        // 配置redis得配置详解
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(redisConnectionFactory);
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.setHashKeySerializer(new GenericJackson2JsonRedisSerializer());
            template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
            template.afterPropertiesSet();
            return template;
        }
    }
    

    8.编写service层

    新建service包以及impl包,这里只提供实现类,接口可以自行编写

    OrderServiceImpl .java:

    import com.spbtrediskill.secondskill.mapper.OrderMapper;
    import com.spbtrediskill.secondskill.pojo.Order;
    import com.spbtrediskill.secondskill.service.OrderService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private OrderMapper orderMapper;
        @Override
        public void createOrder(Order order) {
            orderMapper.insert(order);
        }
    }
    

    StockServiceImpl.java:

    import com.spbtrediskill.secondskill.mapper.StockMapper;
    import com.spbtrediskill.secondskill.pojo.Stock;
    import com.spbtrediskill.secondskill.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    import tk.mybatis.mapper.entity.Example;
    import java.util.List;
    
    @Service
    public class StockServiceImpl implements StockService {
        @Autowired
        private StockMapper stockMapper;
        // 秒杀商品后减少库存
        @Override
        public void decrByStock(String stockName) {
           Example example = new Example(Stock.class);
            Example.Criteria criteria = example.createCriteria();
            criteria.andEqualTo("name", stockName);
            List<Stock> stocks = stockMapper.selectByExample(example);
            if (!CollectionUtils.isEmpty(stocks)) {
                Stock stock = stocks.get(0);
                stock.setStock(stock.getStock() - 1);
                stockMapper.updateByPrimaryKey(stock);
            }
        }
        // 秒杀商品前判断是否有库存
        @Override
        public Integer selectByExample(String stockName) {
            Example example = new Example(Stock.class);
            Example.Criteria criteria = example.createCriteria();
            criteria.andEqualTo("name", stockName);
            List<Stock> stocks = stockMapper.selectByExample(example);
            if (!CollectionUtils.isEmpty(stocks)) {
                return stocks.get(0).getStock().intValue();
            }
            return 0;
        }
    }
    

    9.配置rabbitmq得实现方式以及redis得实现方式
    在 service包下面新建 MQOrderService.java
    这个类属于订单得消费队列

    import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
    import com.spbtrediskill.secondskill.pojo.Order;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    @Service
    @Slf4j
    public class MQOrderService {
        @Autowired
        private OrderService orderService;
        /**
         * 监听订单消息队列,并消费
         *
         * @param order
         */
        @RabbitListener(queues = MyRabbitMQConfig.ORDER_QUEUE)
        public void createOrder(Order order) {
            log.info("收到订单消息,订单用户为:{},商品名称为:{}", order.getOrder_user(), order.getOrder_name());
            /**
             * 调用数据库orderService创建订单信息
             */
            orderService.createOrder(order);
        }
    }
    

    MQStockService.java:
    这个属于库存得消费队列

    import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    @Service
    @Slf4j
    public class MQStockService {
        @Autowired
        private StockService stockService;
        /**
         * 监听库存消息队列,并消费
         * @param stockName
         */
        @RabbitListener(queues = MyRabbitMQConfig.STORY_QUEUE)
        public void decrByStock(String stockName) {
            log.info("库存消息队列收到的消息商品信息是:{}", stockName);
            /**
             * 调用数据库service给数据库对应商品库存减一
             */
            stockService.decrByStock(stockName);
        }
    }
    

     RedisService.java:
    这个配置类,主要用来实现对redis得key和value初始化以及对value得操作

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    @Service
    public class RedisService {
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        /**
         * 设置String键值对
         * @param key
         * @param value
         * @param millis
         */
        public void put(String key, Object value, long millis) {
            redisTemplate.opsForValue().set(key, value, millis, TimeUnit.MINUTES);
        }
        public void putForHash(String objectKey, String hkey, String value) {
            redisTemplate.opsForHash().put(objectKey, hkey, value);
        }
        public <T> T get(String key, Class<T> type) {
            return (T) redisTemplate.boundValueOps(key).get();
        }
        public void remove(String key) {
            redisTemplate.delete(key);
        }
        public boolean expire(String key, long millis) {
            return redisTemplate.expire(key, millis, TimeUnit.MILLISECONDS);
        }
        public boolean persist(String key) {
            return redisTemplate.hasKey(key);
        }
        public String getString(String key) {
            return (String) redisTemplate.opsForValue().get(key);
        }
        public Integer getInteger(String key) {
            return (Integer) redisTemplate.opsForValue().get(key);
        }
        public Long getLong(String key) {
            return (Long) redisTemplate.opsForValue().get(key);
        }
        public Date getDate(String key) {
            return (Date) redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 对指定key的键值减一
         * @param key
         * @return
         */
        public Long decrBy(String key) {
            return redisTemplate.opsForValue().decrement(key);
        }
    }
    

    下面为service包得完整目录:

    10.编写controller层

    在新建得controller包下面新建类 SecController.java

    该controller提供了二个方法,一个为redis+rabbitmq实现高并发秒杀,第二个则用纯数据库模拟秒杀,出现超卖现象

    import com.spbtrediskill.secondskill.config.MyRabbitMQConfig;
    import com.spbtrediskill.secondskill.pojo.Order;
    import com.spbtrediskill.secondskill.service.OrderService;
    import com.spbtrediskill.secondskill.service.RedisService;
    import com.spbtrediskill.secondskill.service.StockService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    @Controller
    @Slf4j
    public class SecController {
        @Autowired
        private RabbitTemplate rabbitTemplate;
        @Autowired
        private RedisService redisService;
        @Autowired
        private OrderService orderService;
        @Autowired
        private StockService stockService;
        /**
         * 使用redis+消息队列进行秒杀实现
         *
         * @param username
         * @param stockName
         * @return
         */
        @PostMapping( value = "/sec",produces = "application/json;charset=utf-8")
        @ResponseBody
        public String sec(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
    
            log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
            String message = null;
            //调用redis给相应商品库存量减一
            Long decrByResult = redisService.decrBy(stockName);
            if (decrByResult >= 0) {
                /**
                 * 说明该商品的库存量有剩余,可以进行下订单操作
                 */
                log.info("用户:{}秒杀该商品:{}库存有余,可以进行下订单操作", username, stockName);
                //发消息给库存消息队列,将库存数据减一
                rabbitTemplate.convertAndSend(MyRabbitMQConfig.STORY_EXCHANGE, MyRabbitMQConfig.STORY_ROUTING_KEY, stockName);
    
                //发消息给订单消息队列,创建订单
                Order order = new Order();
                order.setOrder_name(stockName);
                order.setOrder_user(username);
                rabbitTemplate.convertAndSend(MyRabbitMQConfig.ORDER_EXCHANGE, MyRabbitMQConfig.ORDER_ROUTING_KEY, order);
                message = "用户" + username + "秒杀" + stockName + "成功";
            } else {
                /**
                 * 说明该商品的库存量没有剩余,直接返回秒杀失败的消息给用户
                 */
                log.info("用户:{}秒杀时商品的库存量没有剩余,秒杀结束", username);
                message = "用户:"+ username + "商品的库存量没有剩余,秒杀结束";
            }
            return message;
        }
    }
    

    纯数据库秒杀方式得方法:

        /**
         * 实现纯数据库操作实现秒杀操作
         * @param username
         * @param stockName
         * @return
         */
        @RequestMapping("/secDataBase")
        @ResponseBody
        public String secDataBase(@RequestParam(value = "username") String username, @RequestParam(value = "stockName") String stockName) {
            log.info("参加秒杀的用户是:{},秒杀的商品是:{}", username, stockName);
            String message = null;
            //查找该商品库存
            Integer stockCount = stockService.selectByExample(stockName);
            log.info("用户:{}参加秒杀,当前商品库存量是:{}", username, stockCount);
            if (stockCount > 0) {
                /**
                 * 还有库存,可以进行继续秒杀,库存减一,下订单
                 */
                //1、库存减一
                stockService.decrByStock(stockName);
                //2、下订单
                Order order = new Order();
                order.setOrder_user(username);
                order.setOrder_name(stockName);
                orderService.createOrder(order);
                log.info("用户:{}.参加秒杀结果是:成功", username);
                message = username + "参加秒杀结果是:成功";
            } else {
                log.info("用户:{}.参加秒杀结果是:秒杀已经结束", username);
                message = username + "参加秒杀活动结果是:秒杀已经结束";
            }
            return message;
        }
    

     11.编写springboot启动类
    最后一步我们需要在springboot得启动类中进行对redis得初始化,简而言之就是调用我们上面写得方法,新建一个redis缓存,模拟商品信息

    import com.spbtrediskill.secondskill.service.RedisService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import tk.mybatis.spring.annotation.MapperScan;
    @SpringBootApplication
    @MapperScan("com.spbtrediskill.secondskill.mapper")
    public class SecondskillApplication implements ApplicationRunner{
        public static void main(String[] args) {
            SpringApplication.run(SecondskillApplication.class, args);
        }
        @Autowired
        private RedisService redisService;
        /**
         * redis初始化商品的库存量和信息
         * @param args
         * @throws Exception
         */
        @Override
        public void run(ApplicationArguments args) throws Exception {
            redisService.put("watch", 10, 20);
        }
    }
    

     项目得整个目录:

    至此我们得项目代码就编写完成了,记得仔细检查是否有遗漏,下面准备进入最重要得测试环节!!

    测试前提

    上面代码编写完整之后我们可以启动springboot,启动之前记得打开redis和rabbitmq得服务,检查是否出错:

    启动成功之后打开Redis Desktop Manager工具,查看是否新建了一个redis :watch、

     ok,如果好了,现在打开我们得JMeter工具,可能有些人对这个工具很陌生,下面我教大家如何使用JMeter,大佬忽略!!
    首先选择中文

    完成中文之后,我们在测试计划右键,添加一个线程组

     

    给这个线程组得数量为40,这个线程组得作用就是模拟40个用户发送请求,去秒杀.
    然后再在线程组右键,添加一个Http请求,这个就是我们用来发送请求得组件了

     

    这个请求唯一要说得就是,随机参数了,因为用户名肯定不可能给40个相同得名字,这边我们利用JMeter给用户名得值为随机数
    点击上方得白色小书本,选择random,1-99得随机数:

    然后我们把这个函数字符串复制到http得参数上面去:

     最后我们在测试计划建一个结果树,查看我们发送请求返回得消息数据:

     这些完成之后我们就可以开始发送请求了在这里插入图片描述运行run

    测试结果–redis+rabbitmq

    运行之后查看我们得控制台:

    可以看到日志已经打印到控制台了,用户名为我们生成得随机数。
    再来看下数据库订单表order:

    图中有10条秒杀到商品得用户信息和商品名,我再帮大家理一理,我们初始化得时候给watch库存得数量为10,而我们使用JMeter模拟了40个人发请求,所以这10条数据,也就是40个用户中抢到商品得10个人,也就是线程,谁抢到就是谁得。
    再来查看下我们得结果树:

     

     结果树上面有40条请求信息,通过其中我们可以看的每条请求得详细数据以及返回得值。
    现在我们再打开redismanager,其中我们初始化为10,现在是-30,可以知道有40个线程去获取了它,现在为-30,每次前测试记得,手动清空缓存!!一定要记得

     

    纯数据库方式秒杀结果

    上面我们实现了redis+rabbitmq得秒杀,现在我们看看纯数据库方式得秒杀,看看有什么区别:

    1.首先网stock库存表新增一条数据,类似于redis得初始化
    在这里插入图片描述

    2.在jmeter中修改原来得http请求信息,其中小米对应数据库得商品名

    清空一下结果树,我们开始运行在这里插入图片描述

    3.run
    控制台:

    重要得是查看数据库得信息:
    在这里插入图片描述
    库存已经清空,再看order表

    这样我们可以看到,明明只有10个库存得商品,抢到得人却不止10个,这样明细超卖了,请求树也可以看的超卖信息
    在这里插入图片描述

    总结

    从这二个方式实现得秒杀就可以知道二者得区别,以及大概得了解这个过程是怎么实现得,写这篇文章得主要初衷是方便那些刚接触这方面得小白,没有人刚来什么都会。

  • 相关阅读:
    Flex 学习笔记------组件和视图
    Flex 学习笔记------基于LZMA的文件压缩与上传
    Flex 学习笔记------FLACC & Crossbridge
    Flex 学习笔记------全局事件
    Flex 学习笔记------对象的深层拷贝
    Flex 学习笔记------as 与 js 的通信
    Flex 学习笔记------Local Shared Object 和 Custom Class
    Flex 学习笔记------读取Jpeg图片的width,height和colorSpace
    翻译:eval() 不是魔鬼,只是易被误解
    翻译:javascript 内存管理
  • 原文地址:https://www.cnblogs.com/47Gamer/p/13787306.html
Copyright © 2011-2022 走看看