吃午饭的时间做一个基于Redis的秒杀系统【简易版】

需求

我们有这样一个业务需求:做一场秒杀水果的活动。

功能说明:

  1. 新增水果
  2. 秒杀水果
  3. 查询订单

解决问题

  • 全局唯一【单线程自增实现唯一ID】
  • 超卖问题【乐观锁】
  • 一人一单【分布式锁】
  • 异步秒杀【消息队列】

技术栈

  • JDK 1.8
  • Maven 3.6.3
  • Spring Boot 2.6.3
  • Redis
  • Redisson
  • Mybatis-plus
  • lombok
  • hutool

数据表设计

得此需求,设计数据库及表结构。

水果表结构
在这里插入图片描述
订单表结构
在这里插入图片描述

详细设计

接口

在这里插入图片描述
在这里插入图片描述

Redis相关配置

package com.issa.seckill.config;

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.RedisSerializer;

/**
 * Redis配置类
 *
 * @author issavior
 */
@Configuration
public class RedisConf {
    
    

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    
    


        // 创建Template
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        // key和 hashKey采用 string序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        // value和 hashValue采用 JSON序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);

        return redisTemplate;
    }
}

package com.issa.seckill.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    
    

    @Bean
    public RedissonClient redisClient() {
    
    

        Config config = new Config();
        config.useSingleServer().setAddress("redis://47.96.16.107:6379").setPassword("654321");

        return Redisson.create(config);
    }
//    @Bean
//    public RedissonClient redisClient2() {
    
    
//
//        Config config = new Config();
//        config.useSingleServer().setAddress("redis://47.96.16.107:6380").setPassword("654321");
//
//        return Redisson.create(config);
//    }
//    @Bean
//    public RedissonClient redisClient3() {
    
    
//
//        Config config = new Config();
//        config.useSingleServer().setAddress("redis://47.96.16.107:6381").setPassword("654321");
//
//        return Redisson.create(config);
//    }

}

全局唯一ID

package com.issa.seckill.util;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
@RequiredArgsConstructor
public class RedisUtil {
    
    

    /**
     * redis
     */
    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1661340467;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    /**
     * 获取全局ID:64位= 0 + 时间戳(31)+ 序列号(32位)
     *
     * @param keyPrefix 业务名称
     * @return 返回一个全局ID
     */
    public Long id(String keyPrefix) {
    
    

        LocalDateTime now = LocalDateTime.now();

        // 当前时间和开始时间的时间差
        long timestamp = now.toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP;

        // 生成redis中的key,这里具体到天为key
        String dayKey = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));

        // 自增序列号(在同一天会一直自增,redis单线程自增,以此保证id唯一)
        Long count = stringRedisTemplate.opsForValue().increment("id:" + keyPrefix + ":" + dayKey);

        // 时间戳左移32位,或自增序列号,生成全剧唯一ID
        return timestamp << COUNT_BITS | (count == null ? 0 : count);
    }
}

秒杀代码

package com.issa.seckill.service.serviceImp;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.issa.seckill.bean.Fruits;
import com.issa.seckill.bean.Orders;
import com.issa.seckill.mapper.SeckillMapper;
import com.issa.seckill.service.SeckillService;
import com.issa.seckill.util.RedisUtil;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author issavior
 */
@Service
@RequiredArgsConstructor
public class SeckillServiceImpl extends ServiceImpl<SeckillMapper, Fruits> implements SeckillService {
    
    

    private final SeckillMapper seckillMapper;

    private final RedisUtil redisUtil;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    private final static String SECKILL_CACHE_KEY = "seckill:fruits:";

    /**
     * 新增水果
     *
     * @param fruits 新增的水果数据
     * @return ResponseEntity<Object>
     */
    @Override
    public ResponseEntity<Fruits> insertFruits(Fruits fruits) {
    
    

        long id = redisUtil.id("seckill:fruits");
        fruits.setId(id);
        fruits.setStartTime(fruits.getStartTime());
        fruits.setEndTime(fruits.getEndTime());
        int successFlag = seckillMapper.insertFruits(fruits);
        if (successFlag == 0) {
    
    
            return ResponseEntity.status(400).build();
        }
        stringRedisTemplate.delete(SECKILL_CACHE_KEY + fruits.getName());
        return ResponseEntity.ok(fruits);

    }


    /**
     * 抢购水果<>考虑并发时的基本业务实现~乐观锁~版本号</>
     *
     * @param id 抢购水果的ID
     * @return ResponseEntity<Object>
     */
    @Override
    public ResponseEntity<Object> seckillFruits(Long id) {
    
    

        final String lockKey = SECKILL_CACHE_KEY + id;
        RLock lock = redissonClient.getLock(lockKey);
        try {
    
    
            boolean tryLock = lock.tryLock(10, 10, TimeUnit.SECONDS);
            if (tryLock) {
    
    
                Fruits fruits = this.getById(id);
                ResponseEntity<Object> body = checkFruits(id, fruits);
                if (body != null) return body;
                stringRedisTemplate.delete(SECKILL_CACHE_KEY + fruits.getName());

                // 先创建队列 XGROUP create stream.order g1 0 mkstream

                Long orderId = redisUtil.id("order:");

                Orders orders = new Orders();
                orders.setId(orderId);
                orders.setType(fruits.getName());

                HashMap<String, String> hashMap = new HashMap<>(2);
                hashMap.put("order", JSONUtil.toJsonStr(orders));
                stringRedisTemplate.opsForStream().add("stream.order",hashMap);
            }
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            lock.unlock();
        }
        return ResponseEntity.ok("抢购水果成功,去步尔斯特的主页看看吧!");
    }

    @Nullable
    private ResponseEntity<Object> checkFruits(Long id, Fruits fruits) {
    
    
        if (fruits == null) {
    
    
            return ResponseEntity.status(400).body("水果卖光了,下次再来吧!");
        }
        LocalDateTime startTime = fruits.getStartTime();
        LocalDateTime endTime = fruits.getEndTime();
        Integer count = fruits.getCount();

        if (startTime.isAfter(LocalDateTime.now())) {
    
    
            return ResponseEntity.status(400).body("活动还没开始,去步尔斯特的主页看看吧!!");
        }
        if (endTime.isBefore(LocalDateTime.now())) {
    
    
            return ResponseEntity.status(400).body("活动已经结束了,去步尔斯特的主页看看吧!!");
        }
        if (count < 1) {
    
    
            return ResponseEntity.status(400).body("没有库存了,去步尔斯特的主页看看吧!!");
        }

        // 如果数量和自己查到的数量相等,就购买
        boolean updateBoolean = this.update().setSql("count = count - 1")
                .eq("id", id).gt("count", 0).update();
        if (!updateBoolean) {
    
    
            return ResponseEntity.status(400).body("水果卖光了,下次再来吧!");
        }
        return null;
    }

    @Override
    public ResponseEntity<Fruits> getById(Long id, String name) {
    
    
        final String key = SECKILL_CACHE_KEY + name;

        String result = stringRedisTemplate.opsForValue().get(key);
        if (result != null) {
    
    
            return ResponseEntity.ok(JSONUtil.toBean(result, Fruits.class));
        }

        Fruits fruits = this.getById(id);

        if (fruits != null) {
    
    
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(fruits));
        }

        return ResponseEntity.ok(fruits);

    }
}

源码下载

https://download.csdn.net/download/CSDN_SAVIOR/86497458

如果404,应该是没审核完,等等就好。

猜你喜欢

转载自blog.csdn.net/CSDN_SAVIOR/article/details/126509508