超卖问题通常出现在多用户并发操作的情况下,即多个用户尝试购买同一件商品,导致商品库存不足或者超卖。解决超卖问题的方法有很多:乐观锁、Redis分布式锁、消息队列等。
分布式锁是一种多节点共享的同步机制,通过在多个节点之间协调访问资源,确保在同一时间只有一个节点能够获取锁并执行关键操作。在电商网站中,可以将每个商品的库存作为共享资源,使用分布式锁来控制并发访问。
分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。
● 多进程互斥:同一时刻,只有一个进程可以获取锁
● 保证锁可以释放:任务结束或出现异常,锁一定要释放,避免死锁
● 阻塞锁(可选):获取锁失败时可否重试
● 重入锁(可选):获取锁的代码递归调用时,依然可以获取锁
在实现Redis分布式锁之前,我们先来看看为什么Redis能实现分布式锁:
SETNX lock thread1
DEL lock
EXPIRE lock 10
并且,也不用担心在EXPIRE设置有效期之前服务宕机,Redis的set命令可以满足setnx和expirr的原子性,用一个指令完成两个步骤!
set lock thread1 EX 10 NX
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring:
redis:
port: 6379
host: localhost
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
Boolean secuss = stringRedisTemplate.opsForValue()
.setIfAbsent("lock", "thread", timeoutSec, TimeUnit.SECONDS);
return secuss;
}
@Override
public void unlock() {
stringRedisTemplate.delete("lock");
}
}
@PostMapping("/kill")
public String executeSeckillProduct(int productId,int seckillCount){
//获取锁对象
SimpleRedisLock redisLock =new SimpleRedisLock(stringRedisTemplate);
//尝试获取锁对象
boolean isLock = redisLock.tryLock(1200);
if(!isLock){
return "获取锁失败";
}
Product product = productService.findByPid(productId);
if(product.getStock()<seckillCount){
return "库存不足";
}
product.setStock(product.getStock()-seckillCount);
int row = productService.reduceInventory(product);
if(row>0){
//新增秒杀记录
SeckillRecord record = new SeckillRecord();
record.setPid(product.getPid());
record.setCount(seckillCount);
record.setPanme(product.getPname());
record.setTime(new Timestamp(System.currentTimeMillis()));
recodeService.save(record);
}
//释放锁
redisLock.unlock();
return "秒杀成功";
}
在测试Redis分布式锁之前,我先用了GetWay网关同意了API接口,由网关分发路由请求,通过OpenFegin实现通信并做到负载均衡效果,并对秒杀服务做了节点扩展,尽可能的模拟除了分布式架构:
网关配置:
server:
port: 7000
spring:
application:
name: gateway-application
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: reckill_route
uri: lb://service-reckill
order: 1
predicates:
- Path=/reckill-serv/**
filters:
- StripPrefix=1
先测试不加分布式锁的效果:
数据库库存:10
秒杀记录:0
运行服务:
JMeter测试数据:每秒500个请求
JMeter端口信息:
测试结果:
节点拓展后的三个秒杀服务:
三个服务都共同通过负载均衡消费了请求!
但还要注意的一点是,对于锁的把控一定要按时释放,一次的不释放,可能都会导致后续请求无法成功!
成功撒花!