【笔记】高并发-秒杀架构实战-Redis实现

总体思路

服务端

客户端

org.example.seckill.SeckKillTest2

package org.example.seckill;

import io.lettuce.core.*;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.DecimalFormat;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;

public class SeckKillTest2 {

    private static final Logger logger = LoggerFactory.getLogger(SeckKillTest2.class);


    public static class ProductPutReq {
        private int id;
        private int num;

        public ProductPutReq(int id, int num) {
            this.id = id;
            this.num = num;
        }

        public int getId() {
            return id;
        }

        public int getNum() {
            return num;
        }
    }


    public static class SeckKillReq {
        private int productId;
        private int userId;

        public SeckKillReq(int userId, int productId) {
            this.userId = userId;
            this.productId = productId;
        }

        public int getProductId() {
            return productId;
        }

        public int getUserId() {
            return userId;
        }

    }


    private static final String luaProductPut =
            "local exists = redis.call('EXISTS', KEYS[1]) \n" +
            "if exists == 1 then \n" +
            "   return false \n" +
            "else \n" +
            "   redis.call('HSET', KEYS[1], 'product_id', ARGV[1] ) \n" +
            "   redis.call('HSET', KEYS[1], 'num', ARGV[2] ) \n" +
            "   redis.call('HSET', KEYS[1], 'init_num', ARGV[3] ) \n" +
            "   return true \n" +
            "end";

    //定义 lua脚步
    private static final String luaSeckKill = // 如果已经抢过商品,直接返回
            "local order_id = redis.call('HGET', KEYS[2], ARGV[2]) \n " +
            "if order_id then \n" +
            "   return '-1' \n" +
            "end \r" +
            "local num = tonumber(redis.call('HGET',KEYS[1],'num')) \n" +
            "if num == nil then \n" +
            "   return '-2' \n" +
            "elseif num > 0 then  \n" +
            "   redis.call('HSET', KEYS[1], 'num', num-1) \n" +
            "   local order_id = KEYS[1]..'_'..num \n" +
            "   local res = order_id..string.rep('0', 13 - #order_id) \n" +
            "   redis.call('HSET', KEYS[2], ARGV[2], res) \n " +
            "   return res \n" +
            "else \n" +
            "   return '0' \n"  +  //商品抢完,直接返回 0
            "end ";

    private static final Random random = new Random();

    private static final int base = 100000000;

    private static final int LIMIT = (int)(Runtime.getRuntime().freeMemory() / 1024 / 4 )  ;
    static {
//        logger.info("LIMIT: {}", LIMIT);
        System.out.println("LIMIT: " + LIMIT);
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        int times = args.length > 0 ? Integer.parseInt(args[0]) : 1;

        RedisClient redisClient = RedisClient.create("redis://192.168.253.176:6379");


        SocketOptions socketOptions = SocketOptions.builder()
                .tcpNoDelay(false)
                .build();

        ClientOptions options = ClientOptions.builder()
                .socketOptions(socketOptions)
                .build();
        redisClient.setOptions(options);


        StatefulRedisConnection<String, String> connection1 = redisClient.connect();

        RedisAsyncCommands<String, String> asyncCommands1 = connection1.async();


        RedisAsyncCommands<String, String>[] commands = new RedisAsyncCommands[]{
                asyncCommands1
        };

        //清理上次数据
        logger.info("清理上次数据");
        String prefix = "2";
        RedisFuture<List<String>> delKeys = asyncCommands1.keys(prefix + "*");

        delKeys.get().forEach(asyncCommands1::del);

        asyncCommands1.del(delKeys.get().toArray(new String[0])).get();


        System.out.println("清理上次数据完毕");

        int counter = 0;
        AtomicInteger latchCount = new AtomicInteger(0);
        long begin = System.currentTimeMillis();
        for(int c = 0 ; c < times; c++) {

            int id = base * 2 + random.nextInt(base);
            int num = 1 + random.nextInt(100);

            //商品入库
            ProductPutReq productPutReq = new ProductPutReq( id,  num);
            String key = String.join("_",
                    String.valueOf(productPutReq.getId()));
            String seckKillKey = String.join("_",
                    String.valueOf(productPutReq.getId()),
                    "kill");
            String[] keys = new String[]{key, seckKillKey};
            {

                final String numStr = String.valueOf(productPutReq.getNum());

                RedisFuture<Boolean> future = commands[0].eval(luaProductPut,
                        ScriptOutputType.BOOLEAN, keys, key, numStr, numStr);

                counter++;
                future.whenComplete((r, e) -> {
                    latchCount.incrementAndGet();
                    print(r, e, key,  numStr, asyncCommands1);
                });

            }

            //秒杀商品
            int userNum = num + random.nextInt(100);
            for (int i = 0; i < userNum; i++) {
                SeckKillReq seckKillReq = new SeckKillReq(base + random.nextInt(base),  id);
                {
                    RedisFuture<String> result1 =  commands[0]
                            .eval(luaSeckKill, ScriptOutputType.VALUE, keys, "", seckKillReq.getUserId() + "");
                    counter++;
                    result1.whenComplete((r, e) -> {
                        latchCount.incrementAndGet();
                        printResult(r, e,key, seckKillReq);
                    });
                }
            }

            while ( counter - latchCount.get() > LIMIT )
            {
                Thread.yield();
            }
        }

        while (latchCount.get() < counter)
        {
            Thread.yield();
        }

        long end = System.currentTimeMillis();
        long delta = end - begin;

        Thread.sleep(2000);

        System.out.printf("总计 %s 次请求 %n", counter);
        System.out.printf("总计耗时 %s ms %n", delta);
        System.out.printf("平均耗时 %s ms %n", delta / counter);
        System.out.printf("TPS: %s 次/秒 %n", counter * 1000L / delta);
        System.exit(0);


    }

    private static void print(Boolean r, Throwable e, String key,  String numStr, RedisAsyncCommands<String, String> asyncCommands1) {
        if (e != null) {
            logger.info("商品入库失败, key为 {}, 商品数量为 {}, 异常:{}", key, numStr, e.getMessage() );
        }
        else
        {
            if (r) {
                logger.info("商品入库成功, key为 {}, 商品数量为 {}",  key, numStr);
            } else {
                try {
                    String initNumStr = asyncCommands1.hget(key, "init_num").get();
                    logger.info("商品已经存在, key为 {}, 商品数量为 {}",  key, initNumStr);
                } catch (InterruptedException | ExecutionException ex) {
                    logger.info("商品已经存在, key为 {}, 异常:{}", key, ex.getMessage());
                }

            }
        }
    }

    private static void printResult(String r, Throwable e,String key, SeckKillReq seckKillReq) {
        if (e != null) {
            logger.error("用户 {} 抢商品{}, error", seckKillReq.getUserId(), key, e);
        } else {
            if ("0".equals(r)) {
                logger.info("用户 {} 抢商品 {} 失败,商品已抢完", seckKillReq.getUserId(), key);
            } else if ("-1".equals(r))
            {
                logger.info("用户 {} 抢商品 {} 失败,已经抢过商品", seckKillReq.getUserId(), key);
            } else if ("-2".equals(r)) {
                //乱序处理
                logger.info("用户 {} 抢商品 {} 失败,商品不存在", seckKillReq.getUserId(), key);
            } else
            {
                logger.info("用户 {} 抢商品 {} 订单号: {}", seckKillReq.getUserId(), key, r);
            }
        }
    }

}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>RedisTemplateTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.23.1</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.23.1</version>
            <!--            <scope>test</scope>-->
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.conversantmedia/disruptor -->
        <dependency>
            <groupId>com.conversantmedia</groupId>
            <artifactId>disruptor</artifactId>
            <version>1.2.21</version>
        </dependency>



        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>5.1.4</version>
        </dependency>



        <!-- https://mvnrepository.com/artifact/io.lettuce/lettuce-core -->
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.4.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>


    </dependencies>

</project>

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one or more
  ~ contributor license agreements.  See the NOTICE file distributed with
  ~ this work for additional information regarding copyright ownership.
  ~ The ASF licenses this file to You under the Apache License, Version 2.0
  ~ (the "License"); you may not use this file except in compliance with
  ~ the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="%style{%d{HH:mm:ss.SSS}}{Magenta} %style{|-}{White}%highlight{%-5p} [%t] %style{%40.40c}{Cyan}:%style{%-3L}{Blue} %style{-|}{White} %m%n%rEx{filters(jdk.internal.reflect,java.lang.reflect,sun.reflect)}" disableAnsi="false" charset="UTF-8"/>
        </Console>
        <RollingFile name="RollingFile" fileName="logs/app.log"
                     filePattern="logs/app-%d{yyyy-MM-dd-HH}.log" immediateFlush="false" bufferSize="1048576">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" />
                <SizeBasedTriggeringPolicy size="200MB"/>
            </Policies>
        </RollingFile>
        <Async name="AsyncAppender">
            <AppenderRef ref="RollingFile"/>
            <blocking>false</blocking>
            <LinkedTransferQueue/>
<!--            <DisruptorBlockingQueue/>-->
        </Async>
    </Appenders>
    <Loggers>
        <Root level="info">
<!--            <AppenderRef ref="Console"/>-->
            <AppenderRef ref="AsyncAppender"/>
        </Root>
    </Loggers>

</Configuration>

压测

运行参数

压测结果

猜你喜欢

转载自blog.csdn.net/shumeizwb/article/details/141787197