单机模拟简单秒杀
实现一个简单的秒杀功能,不是很完善,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