实现一个简单的单机秒杀

单机模拟简单秒杀

实现一个简单的秒杀功能,不是很完善,Redis的List结构作为消息中间件,使用Redis的客户端Redisson中的信号量,Semaphore,执行原子性的Lua脚本进行信号量抢占,抢占成功即为秒杀成功,将秒杀成功的信息存储在Redis中,等待业务处理即可。

真实的秒杀肯定没这么简单,要关注的点不仅仅是在如何处理业务,还要关注如何削去流量峰值,将秒杀业务与其他服务隔离开,服务雪崩等等,本人只是一个小白,所了解的也不是很多,如有错误,欢迎指正。

抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。在多线程环境下,限制一定的线程数对某个资源的操作。

  • 首先导入依赖
<!-- redis依赖 -->
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.5.1</version>
</dependency>

<!-- Redisson客户端依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.0</version>
</dependency>
  • 自定义线程池
public class ThreadPool {

    // 阻塞队列
    private static LinkedBlockingQueue queue = new LinkedBlockingQueue();
    // 核心线程数
    private static Integer CorePoolSize = 100;
    // 最大线程数
    private static Integer MaxPoolSize = 1000;
	
	// 构造器私有化
    private ThreadPool(){}

    // 创建自定义线程池
    public static ThreadPoolExecutor pool;
    // 双重校验创建单例线程池
    public static ThreadPoolExecutor getThreadPool(){
        if (pool == null){
            synchronized (ThreadPool.class){
                if (pool == null){
                    pool = new ThreadPoolExecutor(CorePoolSize,MaxPoolSize,10L,TimeUnit.SECONDS,queue);
                }
            }
        }
        return pool;
    }
}
  • 配置RedisTemplate,这里使用的是通过XML来配置
<!--  配置连接池信息  -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxIdle" value="1" />
    <property name="maxTotal" value="5" />
    <property name="blockWhenExhausted" value="true" />
    <property name="maxWaitMillis" value="30000" />
    <property name="testOnBorrow" value="true" />
</bean>

<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
	<!-- redis主机地址 -->
    <property name="hostName" value="192.168.30.135" />
    <!-- redis服务端口 -->
    <property name="port" value="6379"/>
    <property name="poolConfig" ref="jedisPoolConfig" />
    <property name="usePool" value="true"/>
</bean>

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    <property name="connectionFactory"   ref="connectionFactory" />
    <!-- key的序列化方式 -->
    <property name="keySerializer">
        <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    </property>
    <!-- value的序列化方式 -->
    <property name="valueSerializer">
        <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer" />
    </property>
    <property name="hashKeySerializer">
        <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    </property>
    <property name="hashValueSerializer">
        <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
    </property>
</bean>
  • 加载xml配置文件,同样是双重检验
// 同线程池
public class URedisTemplate {

    private URedisTemplate(){}

    public static RedisTemplate redisTemplate;

    public static RedisTemplate getRedisTemplate(){
        if (redisTemplate == null){
            synchronized (URedisTemplate.class){
                if (redisTemplate == null){
                    // 加载配置文件,并创建对象
                    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-redis.xml");
                    redisTemplate = applicationContext.getBean("redisTemplate", RedisTemplate.class);
                }
            }
        }
        return redisTemplate;
    }
}
  • 秒杀业务
// 消息队列的key
public static String TestKey = "test:TestSecKill";
// 信号量的值的key
public static String NumKey = "test:ProductNum";
// redis连接
private static RedisTemplate redisTemplate = URedisTemplate.getRedisTemplate();
// 线程池
private static ThreadPoolExecutor pool = ThreadPool.getThreadPool();
// Redisson连接客户端
private static RedissonClient client = getRedisson();
// 获取Redisson连接客户端
public static RedissonClient getRedisson(){
    Config config = new Config();
    // 单节点模式
    config.useSingleServer().setAddress("redis://192.168.30.135:6379");
    RedissonClient client = Redisson.create(config);
    return client;
}
// 在Redis中准备信号量的值大小
public static void preparedSemaphore(int num){
    if (num <= 0){
        System.out.println("请重新设置数量");
        return;
    }
    BoundValueOperations ops = redisTemplate.boundValueOps(NumKey);
    // 秒杀时间为1分钟
    ops.set(num,1L,TimeUnit.MINUTES);
    System.out.println("数量设置完成");
}
// 模拟发送抢占请求,抢占成功可以发送消息
public static void MessageSend(final BoundListOperations ops,int threadNum){
    // 开启指定个线程进行秒杀模拟
    for (int i = 0; i < threadNum; i++) {
    	// 模拟用户id
        final int userId = i;
        // 提交线程池任务
        pool.execute(new Runnable() {
            public void run() {
                // 获取信号量对象
                RSemaphore semaphore = client.getSemaphore(NumKey);
                // 如果获取到信号量的可用量为0,直接返回,不执行下面逻辑
                if (semaphore.availablePermits() == 0){
                    return;
                }
                // 尝试抢占信号量
                boolean res = false;
                try {
                	// 尝试在10毫秒内获取信号量,成功返回true,失败返回false
                    res = semaphore.tryAcquire(1,10L, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (res) {
                    // 信号量抢占成功,发送消息
                    ops.leftPush(userId + "-成功发送请求!");
                    // 执行业务
                    System.out.println(userId + "-成功发送请求!");
                }else {
                	// 执行抢占失败的业务
                }
            }
        });
    }
    System.out.println("所有请求发送完毕");
    pool.shutdown();
}
// 处理结果
public static void MessageQueue(final BoundListOperations ops){
    Object o = null;
    try {
        while (ops.size() != 0){
        	// 消息出队列
            o = ops.rightPop();
            // 获取用户id,具体以业务为准
            String userId = o.toString().split("-")[0];
            // 业务处理
            System.out.println(userId + "成功秒杀到一个商品!");
            // 测试出错的消息是否会回到消息队列
//                if (ops.size() == 4) {
//                    int i = 10 / 0;
//                }
        }
    } catch (Exception e) {
    	// 如果执行出现异常,将该消息重新入队列
        ops.rightPush(o);
        System.out.println("程序出错,当前处理的消息重新回到队列");
    }
}
  • 在主方法中测试
public static void main(String[] args) {
    long currentTimeMillis = System.currentTimeMillis();
    // 绑定要操作的key
    BoundListOperations ops = redisTemplate.boundListOps(TestKey);
    // 设置10个信号量
    preparedSemaphore(10);
    CompletableFuture.runAsync(() -> {
        // 异步模拟1000个线程同时秒杀
        MessageSend(ops, 1000);
    }, pool);
    // 休眠1秒
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 处理秒杀成功的消息
    MessageQueue(ops);
    System.out.println("执行总共花费->" + (System.currentTimeMillis() - currentTimeMillis));
}
  • 测试结果
数量设置完成
所有请求发送完毕
37-成功发送请求!
34-成功发送请求!
72-成功发送请求!
1-成功发送请求!
18-成功发送请求!
68-成功发送请求!
36-成功发送请求!
75-成功发送请求!
73-成功发送请求!
24-成功发送请求!
18成功秒杀到一个商品!
72成功秒杀到一个商品!
1成功秒杀到一个商品!
34成功秒杀到一个商品!
37成功秒杀到一个商品!
36成功秒杀到一个商品!
75成功秒杀到一个商品!
68成功秒杀到一个商品!
24成功秒杀到一个商品!
73成功秒杀到一个商品!
执行总共花费->1102
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值