搜公众号
推荐 原创 视频 Java开发 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库
Lambda在线 > 黑马程序员成都中心 > 【黑马程序员】redis令牌机制实现秒杀

【黑马程序员】redis令牌机制实现秒杀

黑马程序员成都中心 2018-12-16
举报

广告植入:


成都黑马JavaEE基础22期暂定12月25日开班

名额有限

还在犹豫的小伙伴要抓紧咯

最新消息:

成都黑马前端与移动开发基础01期2019年3月11日开班

成都黑马Python基础01期2019年3月25日开班

首期基础班免费

首期优惠1000元


一、前言1. 秒杀介绍

秒杀是电商系统非常常见的功能模块,是商家进行相关促销推广的常用方式。主要特点是商品库存有限,抢购时间有限。那么在系统设计之初就应该考虑在数量和时间有限的情况下导致的一个高并发以及高并发所带来的库存超卖的问题。秒杀需要解决的问题:1) 库存超卖 解决方案:1) 悲观锁:synchronize 、 Lock2) 乐观锁:数据库乐观锁版本号控制2) 高并发情况下系统压力以及用户体验解决方案: redis本教程采用:redis中list类型达到令牌机制完成秒杀。用户抢redis中的令牌,抢到令牌的用户才能进行支付,支付成功之后可以生成订单,如果一定时间之内没有支付那么就由定时任务来归还令牌

2. 开发介绍

1) 开发工具: IntelliJ IDEA2017.3.52) JDK版本:1.7+3) 数据库: mysql5.7 、 Redis 4) 技术:Spring、Spring Data Redis、mybatis

二、环境搭建1. 数据库表创建

/*商品表 */

CREATE TABLE `goods` (

  `goods_id` int(11) NOT NULL AUTO_INCREMENT,

  `num` int(11) DEFAULT NULL,

  `goods_name` varchar(50) DEFAULT NULL,

  PRIMARY KEY (`goods_id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 

insert  into `goods`(`goods_id`,`num`,`goods_name`) values (1,100,'iphone X');

 

 /*订单表 */

CREATE TABLE `orders` (

  `order_id` int(11) NOT NULL AUTO_INCREMENT,

  `good_id` int(11) DEFAULT NULL,

  `user` varchar(50) DEFAULT NULL,

  PRIMARY KEY (`order_id`)

) ENGINE=InnoDB AUTO_INCREMENT=1163 DEFAULT CHARSET=utf8;

2. redis安装 ( 略 )3. 创建mavne项目,打包方式jar,pom.xml如下

<properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <junit.version>4.12</junit.version>

    <spring.version>4.2.4.RELEASE</spring.version>

    <pagehelper.version>4.0.0</pagehelper.version>

    <mybatis.version>3.2.8</mybatis.version>

    <mybatis.spring.version>1.2.2</mybatis.spring.version>

    <mybatis.paginator.version>1.2.15</mybatis.paginator.version>

    <mysql.version>5.1.32</mysql.version>

    <druid.version>1.0.9</druid.version>

</properties>

 

<dependencies>

    <!-- Spring -->

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-context</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-beans</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-webmvc</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-jdbc</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-aspects</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-jms</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-context-support</artifactId>

        <version>${spring.version}</version>

    </dependency>

    <dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-test</artifactId>

        <version>${spring.version}</version>

    </dependency>

 

    <dependency>

        <groupId>junit</groupId>

        <artifactId>junit</artifactId>

        <version>4.9</version>

    </dependency>

    <dependency>

        <groupId>com.alibaba</groupId>

        <artifactId>fastjson</artifactId>

        <version>1.2.28</version>

    </dependency>

    <dependency>

        <groupId>javassist</groupId>

        <artifactId>javassist</artifactId>

        <version>3.11.0.GA</version>

    </dependency>

    <dependency>

        <groupId>commons-codec</groupId>

        <artifactId>commons-codec</artifactId>

        <version>1.10</version>

    </dependency>

 

    <dependency>

        <groupId>com.github.pagehelper</groupId>

        <artifactId>pagehelper</artifactId>

        <version>${pagehelper.version}</version>

    </dependency>

    <!-- Mybatis -->

    <dependency>

        <groupId>org.mybatis</groupId>

        <artifactId>mybatis</artifactId>

        <version>${mybatis.version}</version>

    </dependency>

    <dependency>

        <groupId>org.mybatis</groupId>

        <artifactId>mybatis-spring</artifactId>

        <version>${mybatis.spring.version}</version>

    </dependency>

    <dependency>

        <groupId>com.github.miemiedev</groupId>

        <artifactId>mybatis-paginator</artifactId>

        <version>${mybatis.paginator.version}</version>

    </dependency>

    <!-- MySql -->

    <dependency>

        <groupId>mysql</groupId>

        <artifactId>mysql-connector-java</artifactId>

        <version>${mysql.version}</version>

    </dependency>

    <!-- 连接池 -->

    <dependency>

        <groupId>com.alibaba</groupId>

        <artifactId>druid</artifactId>

        <version>${druid.version}</version>

    </dependency>

 

    <dependency>

        <groupId>redis.clients</groupId>

        <artifactId>jedis</artifactId>

        <version>2.8.1</version>

    </dependency>

    <dependency>

        <groupId>org.springframework.data</groupId>

        <artifactId>spring-data-redis</artifactId>

        <version>1.7.2.RELEASE</version>

    </dependency>

    <dependency>

        <groupId>dom4j</groupId>

        <artifactId>dom4j</artifactId>

        <version>1.6.1</version>

    </dependency>

    <dependency>

        <groupId>xml-apis</groupId>

        <artifactId>xml-apis</artifactId>

        <version>1.4.01</version>

    </dependency>

</dependencies>

4. 数据访问层

利用mybatis逆向工程生成POJO,以及mapper接口和mapper映射文件。该部分自行操作

mybatis核心配置文件SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <plugins>

        <!-- com.github.pagehelper 为 PageHelper 类所在包名 -->

        <plugin interceptor="com.github.pagehelper.PageHelper">

            <!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL 六种数据库-->

            <property name="dialect" value="mysql"/>

        </plugin>

    </plugins>

</configuration>

数据访问 db.properties

jdbc.driver=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/miaosha?characterEncoding=utf-8

jdbc.username=root

jdbc.password=root

redis配置属性文件redis-config.propertiesproperties

# Redis settings

  # server IP

  redis.host=127.0.0.1

  # server port

  redis.port=6379

  # server pass

  redis.pass=

  # use dbIndex

  redis.database=0

  redis.maxIdle=1000

  redis.maxWait=3000

5. spring配置文件

applicationContext-dao.xml

<!-- 加载配置文件 -->

        <context:property-placeholder location="classpath*:properties/*.properties" />

        <!-- 数据库连接池 -->

        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"

              destroy-method="close">

            <property name="url" value="${jdbc.url}" />

            <property name="username" value="${jdbc.username}" />

            <property name="password" value="${jdbc.password}" />

            <property name="driverClassName" value="${jdbc.driver}" />

            <property name="maxActive" value="10" />

            <property name="minIdle" value="5" />

        </bean>

            <!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->

        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

            <!-- 数据库连接池 -->

            <property name="dataSource" ref="dataSource" />

            <!-- 加载mybatis的全局配置文件 -->

            <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" />

        </bean>

        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

            <property name="basePackage" value="com.miaosha.demo.mapper" />

        </bean>

applicationContext-redis.xml

<context:property-placeholder location="classpath*:properties/*.properties" ignore-unresolvable="true" />  

   

   <!-- redis 相关配置 -->

   <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig"> 

     <property name="maxIdle" value="${redis.maxIdle}" />

       <property name="maxTotal" value="2000" />

 

       <property name="maxWaitMillis" value="${redis.maxWait}" />

     <property name="testOnBorrow" value="true" />

 

   </bean> 

   

   <bean id="JedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"

       p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>

    

   <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">

       <!--<!–开启事务支持–>-->

       <!--<property name="enableTransactionSupport" value="true"/>-->

 

       <property name="connectionFactory" ref="JedisConnectionFactory" />

       <!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->

       <property name="keySerializer">

           <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

       </property>

       <property name="hashKeySerializer">

           <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

       </property>

       <property name="valueSerializer">

           <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

       </property>

       <property name="hashValueSerializer">

           <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>

       </property>

</bean>

applicationContext-service.xml

<context:component-scan base-package="com.miaosha.demo.service" ></context:component-scan><context:component-scan base-package="com.miaosha.demo.service" ></context:component-scan>

三、代码实现

1.定义秒杀业务接口

/**

    * 秒杀的接口

    */

   public interface MiaoShaService {

    

       /**

        * 初始化所有商品的令牌

        * @return

        */

       public boolean initTokenToRedis();

    

       /**

        * 抢购令牌

        * @param goodsId

        * @param user

        * @param num

        * @return

        */

       public boolean miaoshaTokenFromRedis(Integer goodsId,String user,Integer num);

    

       /**

        * 用户未支付 退还令牌

        * @param user

        * @return

        */

       public boolean returnToken(String user);

    

       /**

        * 支付生成订单保存到数据库

        * @param user

        * @return

        */

       public boolean payTokenToOrder(String user);

   }

2. 秒杀业务实现类

@Service

   public class MiaoShaServiceImpl implements MiaoShaService{

 

       @Autowired

       private GoodsMapper goodsMapper;

       @Autowired

       private OrderMapper orderMapper;

    

    

       @Autowired

       RedisTemplate redisTemplate;

    

    

       @Override

       public boolean initTokenToRedis() {

           //查询所有商品

           //根据时间   startTime<=now<=endTime

           //根据状态   已审核状态

          List<Goods> goodsList= goodsMapper.selectByExample(null);

        for(Goods goods : goodsList){

           for(int i=0;i<goods.getNum();i++) {

              //token_goods_1:[token_1_0,token_1_1,token_1_2 ... token_1_99]

              //token_goods_2:[token_2_0,token_2_1,token_2_2 ... token_2_99]

             redisTemplate.boundListOps("token_goods_" + goods.getGoodsId()).leftPush("token_" + goods.getGoodsId() + "_" +i);

              }

          }

           return false;

       }

    

       @Override

       public boolean miaoshaTokenFromRedis(Integer goodsId, String user, Integer num) {

           // 获取令牌

           String token = (String)redisTemplate.boundListOps("token_goods_"+goodsId).rightPop();

           if(token == null || token.equals("")){

               return false;

           }else{

      //记录当前用户已经抢购到令牌,证明当前这个用户可以取支付

      //用redis记录

               String yes  = (String)redisTemplate.boundValueOps(token).get();

if(yes != null && yes.equals("yes")) {

       System.out.println("当前token已经被支付,不能再抢购");

              redisTemplate.boundListOps("token_goods_"+goodsId).remove(1,token);

               return false;

               }

               System.out.println(user);

      redisTemplate.boundHashOps("user_token").put(user,token);

               return true;

           }

       }

    

       @Override

       public boolean returnToken(String user) {

           //获得当前用户的令牌

           String token =  (String) redisTemplate.boundHashOps("user_token").get(user);

           if(token == null || token.equals("")){

               return false;

           }else {

               //得到商品id

               String goodsId = token.split("_")[1];

               redisTemplate.boundListOps("token_goods_"+goodsId).leftPush(token);

               return true;

           }

       }

    

    

       @Override

       public boolean payTokenToOrder(String user) {

           //获得当前用户的令牌

           String token =  (String) redisTemplate.boundHashOps("user_token").get(user);

           if(token == null || token.equals("")){

               return false;

           }else {

          //如果在当前token已经被购买过,那么别人就不能抢当前的token或者不能再对该token进行支付

          //采用redis记录当前token已经被支付

          //redisTemplate.boundValueOps("key").setIfAbsent("value")

          //如果当前这个key有值,该方法就会返回false,如果没有值,就会s设置为对应value,同时返回true

               boolean flag = redisTemplate.boundValueOps(token).setIfAbsent("yes");

               //当前token第一次被支付 flag=true

               if(flag) {

                   //得到商品id

             String goodsId = token.split("_")[1];

             Order order = new Order();

             order.setUser(user);

             order.setGoodId(Integer.parseInt(goodsId));

                orderMapper.insert(order);

                //用户刚好在支付时,定时任务执行

               redisTemplate.boundHashOps("user_token").delete(user);//有可能已经归还token

    

                   redisTemplate.boundListOps("token_goods_" + goodsId).remove(1, token);//移除token, 有可能被别人抢到

               }

               return true;

           }

       }

   }

3. 秒杀测试类

package com.miaosha.test;

     

    import com.miaosha.demo.service.MiaoShaService;

    import org.junit.Test;

    import org.junit.runner.RunWith;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.data.redis.core.RedisTemplate;

    import org.springframework.test.context.ContextConfiguration;

    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

     

    import java.util.ArrayList;

    import java.util.List;

    import java.util.concurrent.CountDownLatch;

     

    @RunWith(SpringJUnit4ClassRunner.class)

    @ContextConfiguration(locations = {"classpath:spring/applicationContext-*.xml"})

    public class MiaoShaTest {

     

        @Autowired

        private MiaoShaService miaoShaService;

        //并发数量

        public static int bfCount = 1000;

        //记录成功个数

        public int count =0;

        //多线程辅助类,控制并发访问数量

        CountDownLatch countDownLatch = new CountDownLatch(bfCount);

     

        @Test

        public void miaoshaToOrder() throws  Exception{

            boolean flag =  miaoShaService.initTokenToRedis();

            if(flag){

               System.out.println("初始化token成功");

            }

            long startTime = System.currentTimeMillis();

            List<Thread> tList = new ArrayList<>();

            for(int i=0;i<bfCount;i++){

                MiaoShaThread miaoShaThread = new MiaoShaThread(1,"user_"+i,1);

                Thread thread = new Thread(miaoShaThread);

                thread.start();

                tList.add(thread);

                countDownLatch.countDown();

            }

            for (Thread t : tList){

                t.join();

            }

            long endTime = System.currentTimeMillis();

            System.out.println("执行时间:"+(endTime-startTime) );

            System.out.println("成功的个数:"+count);

        }

     

     

        @Autowired

        RedisTemplate redisTemplate;

     

        @Test

        public void getTokenFromRefis(){

            Long count = redisTemplate.boundListOps("token_goods_" + 1).size();

            System.out.println("令牌数量:"+count);

        }

     

        @Test

        public void payToOrder(){

            int count_ = 1;

            for(int i=0;i<200;i++){

               boolean b = miaoShaService.payTokenToOrder("user_"+i);

               if(b){

                   count_++;

               }

            }

            System.out.println("支付成功的人数:"+count_);

        }

        //定时任务每秒执行退还令牌的操作

        // 如果在规定时间5分钟之内没有支付就需要退还

        @Test

        public void returnToken(){

            for(int i=0;i<1000;i++) {

                boolean flag = miaoShaService.returnToken("user_"+i);

                if(flag){

                    System.out.println("退还令牌成功");

                }

            }

     

     

        }

     

       class MiaoShaThread implements Runnable{

            private Integer goodsId;

            private String user;

            private Integer num;

            public MiaoShaThread(Integer goodsId,String user,Integer num){

                this.goodsId=goodsId;

                this.user=user;

                this.num=num;

            }

            public void run() {

                try {

                    countDownLatch.await();

                    //操作redis 抢购token

                    boolean flag = miaoShaService.miaoshaTokenFromRedis(goodsId, user, num);

                    if(flag){

                        synchronized (this){

                            count++;

                        }

                    }

     

                }catch (Exception e){

                    e.printStackTrace();

                }

            }

        }

    }

注意:随着CountDownLatch设置并发数量越高,需要调整redis-config.properties属性中的redis.maxIdle属性

四、总结

本文介绍了利用redis的list数据类型模拟令牌队列来完成秒杀,主要解决库存超卖、高并发降低系统压力提高用户体验、解决乐观锁不能先到先得的问题。在单机上运行能够构建上万的请求利用redis抢购100个商品在几秒之内处理完成。本文并不是真是的秒杀业务场景,至少提供一种秒杀的解决思路,如果业务存在某些不确切的地方,欢迎留言交流,相互学习。希望本文能够对您有所帮助


版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《【黑马程序员】redis令牌机制实现秒杀》的版权归原作者「黑马程序员成都中心」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

举报