商品秒杀系统中有那些方法可以保证扣减库存的原子性,即防止超卖

怎么保证原子性操作呢?

1 数据库:
update product set left_num=left_num-1 where left_num>0;

这里用到的是left_num=left_num-1,如果left_num>0才能执行成功,数据库查询、更新的时候有用到锁,是可以保证更新操作的原子性的。
数据库性能较差,不建议使用。

2 分布式锁

分布式锁一般可以用以下方式实现:

  1. 数据库乐观锁;
  2. 基于Redis的分布式锁;
  3. 基于ZooKeeper的分布式锁。

如果用redis来做一个分布式锁,大家都在等待锁解开,不建议使用。
首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:
引入依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

实现分布式锁的代码:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
     String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

解锁代码

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们解锁只需要两行代码就搞定了! 第一行代码,我们写了一个简单的Lua脚本代码。
第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。
eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?
其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

3 消息队列

将订单请求全部放入消息队列,然后另外一个后台程序一个个处理队列中的订单请求。
并发不受影响,但是用户等待的时间较长,进入队列的订单也会很多,体验上并不好,也不建议使用。

4 redis递减/递增

通过 redis->incrby(‘product’, -1) 得到递减之后的库存数。
本系统是递增直到到达售卖数量,即库存量就不再处理请求。
性能方面很好,同时体验上也很好
ps(好是挺好,就是qps不高,只有500左右,对于并发量没有太高要求的项目可以使用这种方法)

5 第三方工具redission或lettuce(最优解)

现在我们就来说说具体的redission的配置,通过配置之后就可以使用了,没用什么特别复杂的地方,先上demo,在根据demo来说说细节。

import java.io.IOException;
 
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class RedisConfig {
	@Bean(destroyMethod = "shutdown")
	RedissonClient redisson() throws IOException {
		Config config = new Config();
		//config.useClusterServers().addNodeAddress("127.0.0.1:6379");
		config.useSingleServer().setAddress("redis://127.0.0.1:6379");
		return Redisson.create(config);
	}
}

redission的配置比较简单,配置一个RedissonClient就可以了,默认的配置支持redis的所有实现方式,如集群,单节点,哨兵,副本等都可以进行配置,这里需要注意的是配置的线程模型,默认配置的线程是64个,我们根据需要来进行配置及连接超时时间,请求超时时间,是否长连接等参数进行配置,这里没有什么特别的参数,基本就是服务器连接的一些参数配置,根据实际业务进行性能优化的点大部分都在这里进行控制,代码的性能优化主要还是根据规范对key的可读性,长度等进行控制,对值得长度进行控制,对结合的元素个数进行控制,以及序列化的性能,主要就是这几方面的处理。

import java.util.concurrent.TimeUnit;
 
import org.redisson.api.RBucket;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
 
@Repository
public class RedisDao {
 
	@Autowired
	private RedissonClient redissonClient;
 
	// -----------------------------------------------------------------------
	public String getString(String key) {
		RBucket<Object> result = this.redissonClient.getBucket(key);
		return result.get().toString();
	}
 
	public void setString(String key, Object value) {
		RBucket<Object> result = this.redissonClient.getBucket(key);
		if (!result.isExists()) {
			result.set(value, 5, TimeUnit.MINUTES);
		}
	}
 
	public boolean hasString(String key) {
		RBucket<Object> result = this.redissonClient.getBucket(key);
		if (result.isExists()) {
			return true;
		} else {
			return false;
		}
	}
 
	public long incr(String key, long delta) {
		return this.redissonClient.getAtomicLong(key).addAndGet(delta);
	}
	// -----------------------------------------------------------------------
 
	public void lock() {
		RCountDownLatch countDown = redissonClient.getCountDownLatch("aa");
		countDown.trySetCount(1);
		try {
			countDown.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
 
		RCountDownLatch latch = redissonClient.getCountDownLatch("countDownLatchName");
		latch.countDown();
		RReadWriteLock rwlock = redissonClient.getReadWriteLock("lockName");
		rwlock.readLock().lock();
		rwlock.writeLock().lock();
		rwlock.readLock().lock(10, TimeUnit.SECONDS);
		rwlock.writeLock().lock(10, TimeUnit.SECONDS);
		try {
			boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
			boolean res1 = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
 
	}
}

redission中包含了我们了解的常用锁的类型,基本的可重入锁,读写锁,以及CountDownLatch的设置及使用,但是他们是分布式锁,以往我们JUC提供的锁都是在单线程的线程模型中使用的,当多个进程多个线程来操作一个无锁的共享资源的时候,就会出现线程不安全的问题,就是我们多次执行后结果和单个线程执行时结果的不一致,为了让线程一致我们是需要一些处理办法的,那就是分布式锁,通过锁进行多线程的同步来进行资源隔离来实现对资源的访问控制,从而达到线程安全,所以根据实际中的业务需要,我们可以根据自身的技术实力及业务需要来实现自己的分布式锁,实现的方式主要有redis和zookeeper,有需要的可以自行百度学习,稍后博主自己也会对该方面知识进行学习整理。通过上面的整理相信大家已经对redission已经有了一个初步的认识,具体的编程语法需要自己去根据数据类型具体学习。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三七有脾气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值