1.业务描述
模拟一个商品的下订单业务。
用户下订单
第一步:用户单击下单接口
第二步:执行下单业务
-
先从Redis总获取库存,判断库存是否充足
-
当库存>=下单数量时
扣减库存
下单成功
扣减库存
-
当库存<下单数量时
库存不足 下单失败
2.超卖问题
商品超卖是指在电子商务系统或其他销售系统中,某个商品的库存数量被错误地减少到负数,导致卖出的商品数量超过了实际库存数量。这是一个常见的问题,特别是在高并发的情况下,可能会出现多个用户同时购买同一件商品,而系统没有正确处理这种竞争条件,导致超卖。
超卖问题可能会导致以下不良后果:
-
库存不准确:超卖会导致库存数量不准确,系统显示的库存与实际库存不一致。
-
用户体验差:用户购买的商品实际上已经售罄,但系统仍然接受订单,这会给用户带来不满和失望。
-
损失和纠纷:商家需要履行超卖订单,或者退款给用户,这可能导致商家的损失和用户不满,甚至引发纠纷。
为了解决商品超卖问题,可以采取以下一些策略:
锁定库存:在用户下单时,首先锁定相应数量的库存。这可以通过分布式锁或数据库事务来实现。锁定库存后,其他用户将无法购买相同商品,直到订单完成或取消。
-
扣减库存原子操作:扣减库存应当是一个原子操作,确保只有一个线程可以修改库存数量。这可以通过数据库事务或原子性的内存操作来实现。
-
超卖检测:在扣减库存时,需要检查库存是否足够。如果库存不足,应拒绝订单或采取其他措施,而不是扣减库存。
-
队列管理:使用队列来处理订单,确保每个订单按照先后顺序进行处理。这可以避免竞争条件。
-
库存同步:定期或实时同步库存数量,确保库存数量准确。
-
使用分布式锁:在分布式系统中,使用分布式锁可以避免不同节点之间的竞争条件。
解决商品超卖问题需要综合考虑业务逻辑、并发控制和系统架构等因素,以确保系统的高可用性和数据一致性。
3.使用Redis分布式锁解决
3.1 版本1
定义一个锁,尝试使用锁来控制多线程并发,实现多个线程同步访问Redis库,解决了单体项目超卖问题。
public String sale1(int count){
String retMessage = "";
lock.lock();
try{
// 1.查询库存
String resultFromRedis=stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
lock.unlock();
}
return retMessage;
}
缺点:无法解决分布式环境下产品超卖问题。
3.2 版本2
利用Redis的setnx命令,实现分布式锁,以递归的方式重试,解决了分布式环境下商品超卖问题。
public String sale2(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 尝试获取
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if(!flag){
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 递归尝试获取锁
sale2(count);
}else{
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
stringRedisTemplate.delete(key);
}
}
return retMessage;
}
缺点:由于使用了递归调用,当持有锁的线程,长时间未释放锁时,容易出现栈溢出。
3.3 版本3
利用Redis的setnx命令,实现分布式锁,以自旋的方式重试,解决了版本2中的栈溢出问题。
public String sale3(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 循环尝试获取
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage;
}
缺点:当持有锁的微服务宕机时,锁没法释放,导致后面微服务无法获取锁。
3.4 版本4
在版本3的基础上,加入key的过期时间,解决了版本3出现的微服务无法获取锁的问题。
public String sale4(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 循环尝试获取
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
//设置过期时间
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage;
}
缺点:加锁和设置过期时间不是原子操作,出现原子问题
3.5 版本5
在版本4的基础上,加锁和设置时间使用一条原子命令,解决了版本4的问题
public String sale5(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 循环尝试获取
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage;
}
缺点:由于多线程并发,引入误删除锁问题
3.6 版本6
只允许自己删除自己的锁,不可以删除别人添加的锁。
public String sale6(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 循环尝试加锁
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
//删除之前判断下是不是自己加的锁
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)){
stringRedisTemplate.delete(key);
}
}
return retMessage;
}
缺点:判断是不是自己的锁和删除操作不具有原子性
3.7 版本7
使用lua脚本解决版本6出现的问题。
public String sale7(int count){
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
// 循环尝试加锁
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
//lua脚本:先判断一个key的value是否跟预期的一样,一样的话就删除这个key,否则什么也不做
// if(条件) then //小括号可省略
// 业务代码
// elseif(条件) then
// 业务代码
// elseif(条件) then
// 业务代码
// elseif(条件) then
// 业务代码
// ...
// else
// 业务代码
// end //if终止符
// eval "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock-test 112233
// lua脚本执行语法:EVLA "脚本" 参数
//删除之前判断下是不是自己加的锁
String lua_script=
"if redis.call('get',KEYS[1]) == ARGV[1] " +
"then return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(lua_script, Boolean.class), Arrays.asList(key), value);
}
return retMessage;
}
缺点:当业务方法调用其他方法且其他方法需要加分布式锁的时候,未考虑可重入性,这个时候会其他方法也会尝试加锁。
3.8 版本8
考虑可重入性,维护一个重入度,使用hset key value recount,为了保证多个命令原子性,使用lua脚本。
public String sale8(int count){
// 加锁的lua脚本,等价于lock方法
// if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then
// redis.call('hincrby',KEY[1],ARGV[1],1)
// redis.call('expire',KEY[1],ARGV[2])
// return 1
// else
// return 0
// end
// 解锁的lua脚本,等价于unlock方法
// if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then
// return nil
// elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then
// return redis.call('del',KEY[1])
// else
// return 0
// end
String retMessage = "";
String key = "redis-lock";
String value = UUID.randomUUID()+":"+Thread.currentThread().getId();
Integer expireTime = 50;
String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
" redis.call('hincrby',KEY[1],ARGV[1],1)" +
" redis.call('expire',KEY[1],ARGV[2])" +
" return 1" +
" else" +
" return 0" +
" end";
// 循环尝试加锁
while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock, Boolean.class), Arrays.asList(key), value, String.valueOf(expireTime))){
try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}
}
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
String lua_unlock="if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then" +
" return nil" +
" elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then" +
" return redis.call('del',KEY[1])" +
" else" +
" return 0" +
" end";
stringRedisTemplate.execute(new DefaultRedisScript<>(lua_unlock, Long.class), Arrays.asList(key), value);
}
return retMessage;
}
缺点:耦合度高
3.9 版本9
将锁封装为一个类,实现Lock规范,降低耦合
private MyRedisLock myRedisLock = new MyRedisLock(stringRedisTemplate,"redis-lock");
public String sale9(int count){
String retMessage = "";
myRedisLock.tryLock();
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
myRedisLock.unlock();
}
return retMessage;
}
自定义锁实现Lock接口
public class MyRedisLock implements Lock {
private StringRedisTemplate stringRedisTemplate;
private String key;
private String value;
private long expireTime;
public MyRedisLock(StringRedisTemplate stringRedisTemplate, String key,String value) {
this.stringRedisTemplate = stringRedisTemplate;
this.key = key;
this.value= value;
this.expireTime = 50L;
}
public MyRedisLock(StringRedisTemplate stringRedisTemplate, String key) {
this.stringRedisTemplate = stringRedisTemplate;
this.key = key;
this.value= UUID.randomUUID()+":"+Thread.currentThread().getId();
this.expireTime = 50L;
}
@Override
public void lock() {
tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {tryLock(-1L, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {e.printStackTrace();}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time == -1){
String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
" redis.call('hincrby',KEY[1],ARGV[1],1)" +
" redis.call('expire',KEY[1],ARGV[2])" +
" return 1" +
" else" +
" return 0" +
" end";
while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock,Boolean.class), Arrays.asList(key),value,String.valueOf(expireTime))){
TimeUnit.MILLISECONDS.sleep(50);
}
System.out.println("锁的名字:"+key);
return true;
}
return false;
}
@Override
public void unlock() {
String lua_unlock="if(redis.call('exist',KEY[1],ARGV[1]) ==0 ) then" +
" return nil" +
" elseif(redis.call('hincrby',KEY[1],ARGV[1],-1) == 0) then" +
" return redis.call('del',KEY[1])" +
" else" +
" return 0" +
" end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(lua_unlock, Long.class), Arrays.asList(key), value);
System.out.println("锁的名字:"+key);
if(null == flag){
throw new RuntimeException("当前锁不存在!");
}
}
@Override
public Condition newCondition() {
return null;
}
}
缺点:通用性不好
3.10 版本10
引入工厂模式,实现通配型
@Autowired
private MyLockFactory myLockFactory1;
public String sale10(int count){
Lock myRedisLockFromFactory = myLockFactory1.getLock("REDIS");
String retMessage = "";
myRedisLockFromFactory.tryLock();
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
myRedisLockFromFactory.unlock();
}
return retMessage;
}
工厂类:
@Component
public class MyLockFactory {
private String key;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private String value;
public MyLockFactory() {
this.value = UUID.randomUUID()+":"+Thread.currentThread().getId();
}
public Lock getLock(String lockType){
if(lockType == null) return null;
if(lockType.equalsIgnoreCase("REDIS")){
this.key="redis-lock";
return new MyRedisLock(stringRedisTemplate,key,value);
}else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
this.key="zookeeper-lock";
return null;//返回zookeeper版本的分布式锁
}else if(lockType.equalsIgnoreCase("MYSQL")){
this.key="zookeeper-lock";
return null;//返回MySQL版本的分布式锁
}else
return null;
}
}
缺点:没有自动续期
3.11 版本11
定时任务+lua脚本实现自动续期
@Autowired
private MyLockFactory myLockFactory2;
public String sale11(int count){
// 自动续期脚本
// if(redis.call('hexists',KEY[1],ARGC[1] ==1 ) then
// return redis.call('expire',KEY[1],ARGV[2])
// else
// return 0;
// end
Lock myRedisLockFromFactory = myLockFactory2.getLock("REDIS");
String retMessage = "";
myRedisLockFromFactory.tryLock();
try{
// 1.查询库存
String resultFromRedis = stringRedisTemplate.opsForValue().get(key);
// 判断库存是否足够
Integer num=resultFromRedis == null?0:Integer.parseInt(resultFromRedis);
if(num-count > 0){
stringRedisTemplate.opsForValue().set(key,String.valueOf(--num));
retMessage = "成功卖出"+count+"件商品"+"服务端口号:"+serverPort;
System.out.println(retMessage+"当前商品剩余:"+(num+1-count)+"件。");
// 模拟长业务
try {TimeUnit.SECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}
}else{
retMessage="商品已售罄!非常抱歉!";
System.err.println(retMessage);
}
}finally {
myRedisLockFromFactory.unlock();
}
return retMessage;
}
在自定义分布式锁加入定时任务:
private void reNewExpire() {
String lua_reNewExpire="if(redis.call('hexists',KEY[1],ARGC[1] ==1 ) then" +
" return redis.call('expire',KEY[1],ARGV[2])" +
" else" +
" return 0;" +
" end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if(stringRedisTemplate.execute(new DefaultRedisScript<>(lua_reNewExpire, Boolean.class), Arrays.asList(key), value, String.valueOf(expireTime))){
reNewExpire();
}
}
},(this.expireTime*1000) / 3);//每三分之一过期时长执行一次
}
重写加锁流程:
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time == -1){
String lua_lock="if(redis.call('exist',KEY[1],ARGV[1]) == 0 or redis.call('hexist',KEY[1],ARGV[1]) == 1) then" +
" redis.call('hincrby',KEY[1],ARGV[1],1)" +
" redis.call('expire',KEY[1],ARGV[2])" +
" return 1" +
" else" +
" return 0" +
" end";
while(!stringRedisTemplate.execute(new DefaultRedisScript<>(lua_lock,Boolean.class), Arrays.asList(key),value,String.valueOf(expireTime))){
TimeUnit.MILLISECONDS.sleep(50);
}
// 后台程序,自动续期
reNewExpire();
System.out.println("锁的名字:"+key);
return true;
}
return false;
}
缺点:安全性不能提供保证,例如,当Redis服务器宕机。
改进:使用基于Redis官方提供的RedLock算法来实现多个实例下的分布式锁。例如基于Java的Redisson。
4.总结
使用Redis实现分布式锁是一种有效的方式来管理并发访问共享资源。Redis提供了原子性的操作,特别适合用于构建分布式锁。在这种锁的设计中,每个请求都尝试获取锁,只有一个请求成功,其他请求被阻塞。利用Redis的SETNX(SET if Not eXists)命令,可以确保锁的原子性,从而避免竞争条件。
这种分布式锁的优点包括快速响应、可靠性和简单性。但需要注意的是,为了避免死锁,锁应该具有超时机制,以确保即使持有锁的客户端崩溃,锁最终会被释放。此外,分布式锁的实现需要谨慎处理锁的释放,确保只有持有锁的客户端才能释放它。总之,Redis分布式锁是构建分布式系统中的重要工具,可用于解决并发访问共享资源时的竞争条件问题。
但是需要考虑更多的细节,一步一步改进。