需求
我们有这样一个业务需求:做一场秒杀水果的活动。
功能说明:
- 新增水果
- 秒杀水果
- 查询订单
解决问题
- 全局唯一【单线程自增实现唯一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,应该是没审核完,等等就好。