1秒杀抢购业务特点
秒杀抢购是电商平台的一种业务模式,它可以聚焦流量和吸引注意力,通过秒杀活动长时间吸引人群,最终吸引用户下单。
秒杀抢购业务有哪些特点呢?
时间限制: 秒杀抢购活动通常在特定的时间段内进行,例如1小时或更短的时间。在这个时间段内,消费者可以购买特定商品或服务,通常是限量销售。
限量销售: 秒杀抢购商品通常数量有限,销售数量是提前确定的。一旦库存售罄,抢购活动就结束,未能购买的消费者需要等待下一次活动。
价格优惠: 秒杀抢购商品通常以折扣价格销售,价格较平时低廉。这种价格优势是吸引消费者参与抢购的重要因素。
高并发和服务器压力: 抢购开始时大量用户会同时访问在线商城,导致网站服务器承受巨大压力。因此,网站的服务器和网络基础设施需要具备高并发处理能力,以应对瞬时大量的用户请求。
技术要求高: 秒杀抢购业务对技术要求非常高,包括网站性能优化、数据库优化、缓存技术、负载均衡等方面的技术应用。
2 常用技术方案
实现秒杀抢购业务会用到哪些技术呢?可以参考行业上一些成熟的解决方案。
-
缓存方案
使用缓存技术(如Redis)来存储热点数据,例如商品信息和库存数量。这样可以减轻数据库的压力,提高读取数据的速度。
-
异步处理方案
当用户成功秒杀后,将抢购信息发送到队列,然后由消费者多线程异步处理订单,减轻系统的实时压力,使用Redis、RabbitMQ等技术都可以实现队列。
-
防止超卖方案
超卖是最终下单购买数量大于库存数量,比如:库存100个用户最终购买了101个,多出这一个就是超卖了,在秒杀抢购业务中这也是需要解决的问题,可以使用分布式锁、Redis等技术都可以防止超卖。
-
限流与防刷方案
使用限流算法(如令牌桶、漏桶算法)来控制请求的并发数,防止服务器被过多请求压垮。可以在服务端使用限流技术,比如:sentinel、nginx、验证码等技术。
-
数据库优化方案
对数据库进行优化,包括索引的设计、SQL语句的优化、数据库连接池的使用等,以提高数据库的查询和更新速度。
-
数据库分库分表方案
在数据库层面进行分库分表,将数据分散存储在不同的数据库实例或表中,提高数据库的读写性能。
-
负载均衡
使用负载均衡技术,例如Nginx、Spring Cloud Gateway等,将请求分发到多个服务器上,增加系统的处理能力。
-
CDN加速
CDN(Content DeliveryNetwork)即内容分发网络,CDN用于加速静态资源的访问,将内容分发到CDN节点就近为客户提供服务。
3. 面向高并发如何提高活动查询性能?
如果直接查询数据库无法满足需求并且对数据库造成巨大的压力从而影响其它功能使用数据库,我们可以使用缓存,将优惠券活动信息存入缓存,比如Redis,从Redis查询避免查询数据库
4. 解决超卖问题
导致超卖问题的原因是多线程安全问题。因此解决该问题的核心思想是锁。
高并发场景不推荐使用select … for update方法,同时也可能存在死锁的潜在风险
因此一般追求速度的分布式锁选择redisson。此外也可以使用zookeeper的分布式锁方案。
redisson的基本用法:
// 创建Redisson客户端
RedissonClient redissonClient = Redisson.create();
// 获取名为myLock的分布式锁实例,通过此实例进行加锁、解锁
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试获取锁,最多等待3秒,持锁时间为5秒
boolean isLockAcquired = lock.tryLock(3, 5, TimeUnit.SECONDS);
if (isLockAcquired) {
// 获取锁成功,执行业务逻辑
} else {
// 获取锁失败,处理相应逻辑
}
} catch (InterruptedException e) {
// 处理中断异常
} finally {
// 释放锁
lock.unlock();
}
说明:
lock.tryLock方法是一种非阻塞获取锁的方式,没有获取锁可以直接返回,而lock.lock()是一种阻塞获取锁的方法,多个线程通过lock()方法获取锁,只有一个线程获取到锁,其它线程将阻塞等待。
通常lock.tryLock方法使用的更广泛。
-
使用tryLock方法获取锁时传3个参数:
-
waitTime:尝试获取锁的最大等待时间,在这个时间范围内会不断地尝试获取锁,如果在
waitTime
时间内未能获取到锁,则返回false
。waitTime默认为-1,表示获取锁失败后立刻返回不重试。 -
leaseTime:表示持锁的时间,即锁的自动释放时间。在获取锁成功后,锁会在
leaseTime
时间后自动释放。如果在持锁的时间内未手动释放锁,锁也会在leaseTime
时间后自动释放。 -
TimeUnit:表示时间单位,可以是秒、毫秒等。
-
tryLock方法返回值:
true:获取到了锁
false:未获取到锁
-
注意释放锁
lock.tryLock代码放在try中,在finally 中释放锁。
思考Redisson现在有一个问题:
当设置了leaseTime的时间为10秒,结果任务执行了20秒,会出现什么问题?
由于锁的自动释放时间为10秒,当到达到10秒即使任务还没有结束锁将自动释放,此时就会有新线程获取该锁去执行任务,设置分布式锁的本意是当前只有一个线程去执行,出现这个问题会导致多个线程共同去执行任务,可能在并发处理上存在问题。
当执行任务的时间可以控制在一个范围就可以指定leaseTime锁自动释放时间,如果执行任务的时间不容易通过leaseTime去设置,此时可以使用Redisson的看门狗机制避免在任务没有完成时自动释放锁的问题发生。
Redisson的"看门狗机制"(Watchdog)是一种用于监测和维护锁的超时时间的机制,它可以确保在任务没有完成时对锁的过期时间进行自动续期,以避免任务没有完成时锁自动释放的问题。
开启看门狗后针对当前锁创建一个线程执行延迟任务,默认每隔10秒将锁的过期时间重新续期为30秒。
看门狗线程会首先判断锁是否存在,如果不存在将不再续期,当程序执行unlock()方法释放锁时会将该锁的对应的延迟任务取消,此时看门狗线程结束任务。
注意:任务结束一定要执行unlock()方法释放锁,否则看门狗线程一直进行续期,导致锁无法释放。
Apache Curator-分布式锁的另一种方案:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version> <!-- 请根据需要选择版本 -->
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.0</version>
</dependency>
创建客户端:
private CuratorFramework client;
public void getConnect(){
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
client = CuratorFrameworkFactory.builder()
.connectString(zookeeperConnectionString)
.sessionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.build();
client.start();
}
@Autowired
private curatorFramework curatorFramework;
public void deduct(){
// 加锁,获取锁失败重试
InterProcessMutex mutex = new InterProcessMutex(curatorframework, "/curator/lock");
try {
mutex.acquire();
// 1.查询库存信息string stock=redisTemplate.opsForValue().get("stock");
// 2.判断库存是否充足
if(stock != null && stock.length()!=0){
int st = Integer.parseInt(stock);
if(st >0){
// 3.扣减库存
redisTemplate.opsForValue().set("stock",string.valueof(--st));
}catch(Exception e){
e.printStackTrace();
} finally {
// 释放锁
try
mutex.release();
}catch(Exception e){
e.printStackTrace();
}
}
可重入读写锁InterProcessReadWriteLock