⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_43074462/article/details/103756536 「控场的朴哥」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

写在前面

需求是做一个秒杀系统,比如大家来抢100台手机,先到先得。

查阅了网上很多用redis实现秒杀的demo(java语言),竟然没一个能用的!!!

有些是php的,没闲心研究了,现在说说为什么不能用:

  • 绝大多数的DEMO都是基于redis的watch特性的事务实现①,
  • 个别是基于redis分布式锁实现②。
  • 当然还有些用了脚本的,我也没仔细看是lua还是调用redis指令,哪有那个闲心去研究哇。

照顾一下小白,分析一下为什么这几种实现不行

1.基于watch特性的 不靠谱 实现

其实这两种实现方式,完全可以理解为乐观锁(watch)和悲观锁(加分布式锁)

watch事务,相当于是乐观锁,这种方法在并发情况下极为不靠谱,假设有100个人同时尝试秒杀,那么极端情况下,有99个人都会失败,只有一个能修改成功。

然而demo里甚至没写如果修改失败了就重试这个功能,那显然这失败的99个人,已经提示失败了,过一会回来,发现还剩了90多。那我是怎么失败的?我替他们问问了。

并且使用这种方式实现呢,在并发量较大的时候,过多的重试线程应该会严重影响服务器性能。

2.基于用redis做个分布式锁的 不靠谱 实现

这种实现方式相当于一个悲观锁,每次执行减减操作之前,在redis中存入一个k,v键值对,使用特定的名称,并且使用setNX特性,确保抢锁没有安全问题,并在使用完成后释放锁。那么问题是,在100个人秒杀时,只有一个人抢到锁,剩下99个人怎么办?

demo里同样没写个重试,抢不到锁就失败,醉了,不过就算写重新抢锁的机制,那么几十个上百个线程不断抢锁,想想是个挺恐怖的事,更别提高并发了。

基于脚本的实现 不靠谱 实现

作为一个C系语言开发,我看不太懂,看不懂就是不靠谱,出了问题都不知道改哪里,你说靠不靠谱

正题:使用spring操作redis的list队列实现

我用的是springboot的StringRedisTemplate,至于如何整合jedis到spring等等,去查阅其他文章吧,我就不重复写了。

贴工具类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collection;

@Service
public class RedisServiceImpl<T> implements RedisService<T> {
@Autowired
private StringRedisTemplate stringRedisTemplate;

//添加字符串并设置过期时间
@Override
public void addString(String key, String value, Duration duration) {
stringRedisTemplate.opsForValue().set(key, value, duration);
}

//查找字符串
@Override
public String findString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}

//根据Key删除
@Override
public Boolean deleteByKey(String key) {
return stringRedisTemplate.delete(key);
}

//在队列尾部减少一个对象
@Override
public String removeOneEntryOnListRight(String listName) {
return stringRedisTemplate.opsForList().rightPop(listName);
}

//在队列头部新增对象
@Override
public Long addEntriesOnListLeft(String listName, Collection<String> args) {
return stringRedisTemplate.opsForList().leftPushAll(listName, args);
}

}

解释一下哈 这个类的父类是我自己写的service层,不是提供好的

主要使用的是最后两个方法,最后一个方法,在队列头部新增对象,如果没有这个队列,他会创建出来这个队列,然后将一个集合统统塞到这个redis队列中。倒数第二个方法每调用一次,会删除队列中最后一个元素,然后返回这个元素的值,如果队列中已经没有元素了(队列已经没了)那么他会返回null,他们都是原子操作。

如此,每个请求都无需经过加锁操作,直接利用redis的单线程特性,即可实现高并发下的秒杀:请求到达redis,redis会逐个执行,每一次执行要么返回一个值,要么返回null。很显然,返回值的就是抢到了,返回null的就是没抢到。而且可以灵活的为这个队列新加入一些元素(老板发话再加100台)或者直接把这个队列删了(老板说不行,不卖了)都不会对代码产生任何影响。

其中对应的redis操作指令分别是:

  • 在队列左侧新增:lpush
  • 在队列右侧消费:rpop

老板不卖了:del (笑)

接下来贴出十分简单的使用方法

先贴在任务开始时向redis中插入一个大队列

List<String> entriesList = new LinkedList<>();
for (int i = 0; i < 100; i++){
entriesList.add("某个商品");
}
redisService.addEntriesOnListLeft("队列名",entriesList);

突然想到这个实现即使秒杀100台不同型号的手机(并且在秒到时就通知用户秒到的是啥),也不用改代码。

每次秒杀执行:

String redisResult = redisService.removeOneEntryOnListRight("队列名");
if (null == redisResult) {
//说明没抢到
}else{
//说明抢到了 执行抢到逻辑
}

突然发现这个实现看起来甚至比那些所谓的秒杀demo还简单

但他既没有并发问题,也没有为了解决并发问题而衍生的性能问题。

虽然没经过测试,不过我认为就算秒杀10万台,放到redis队列里,应该也占用不了多少内存。

赶工分布式秒杀,没想到如此基本的内容,竟没找到一个靠谱的实现,从上午写到现在(周末晚8点)一顿饭没吃,被网上过于demo的资源逼的,忍饿怒出此文。

文章目录
  1. 1. 写在前面
  2. 2. 1.基于watch特性的 不靠谱 实现
  3. 3. 2.基于用redis做个分布式锁的 不靠谱 实现
  4. 4. 基于脚本的实现 不靠谱 实现
  5. 5. 正题:使用spring操作redis的list队列实现
    1. 5.0.1. 接下来贴出十分简单的使用方法