Redis增减库存避坑的实现


    目录
  • Redis实现库存管理
    • 查询商品库存数量
    • 更新商品库存数量
    • 判断商品库存是否充足
    • 避免超卖问题
  • 问题
  • 推荐解决方案
  • 问题描述
  • 解决方案

    Redis实现库存管理
    查询商品库存数量
    首先,我们可以使用Redis的String类型来存储商品的库存数量。每个商品对应一个key,其值为库存数量。当需要查询商品库存数量时,只需要获取相应key的值即可。
    
# 获取商品库存数量
def get_stock(product_id):
    redis_conn = Redis()
    stock = redis_conn.get(f'stock:{product_id}')
    return int(stock) if stock else 0

    更新商品库存数量
    当有人购买商品时,我们需要更新商品的库存数量。为了保证库存的准确性,我们可以使用Redis的原子操作INCRBY或者DECRBY来实现库存数量的增减。
    
# 更新商品库存数量
def update_stock(product_id, quantity):
    redis_conn = Redis()
    redis_conn.incrby(f'stock:{product_id}', quantity)

    判断商品库存是否充足
    为了判断商品库存是否充足,我们只需要查询商品的库存数量并与购买数量进行比较即可。
    
# 判断商品库存是否充足
def check_stock(product_id, quantity):
    stock = get_stock(product_id)
    return stock >= quantity

    避免超卖问题
    在高并发的情况下,可能会出现超卖问题,即多个用户同时购买了同一件商品,导致库存数量出现负数。为了避免这个问题,我们可以使用Redis的WATCH机制来保证库存数量的原子性。
    
# 避免超卖问题
def avoid_over_sell(product_id, quantity):
    redis_conn = Redis()
    with redis_conn.pipeline() as pipe:
        while True:
            try:
                pipe.watch(f'stock:{product_id}')
                stock = pipe.get(f'stock:{product_id}')
                stock = int(stock) if stock else 0
                if stock < quantity:
                    pipe.unwatch()
                    raise Exception('库存不足')
                pipe.multi()
                pipe.decrby(f'stock:{product_id}', quantity)
                pipe.execute()
                break
            except WatchError:
                continue

    问题
    先执行get获取值,判断符合条件再执行incr、decr操作。在临界缓存失效的情况下,会默认赋值当前key为永不过期的0,再执行加减法,导致程序异常。
    推荐解决方案
    1、限制接口频率:先incr,执行后值为1,说明是第一次执行,需要额外设置过期时间,再判断是否超过当前接口频率限制(注意上述步骤不可调换顺序)
    2、使用lua脚本完整提交一次操作,脚本中的key可以保证一致。以加减库存为例,先查询key存在的情况下,再进行库存变更,如果不存在无需处理,等待下次缓存加载即为最新的值
    问题描述
    场景1:我们缓存了一个商品的库存,过期时间为5分钟,根据用户的购买和取消执行 incr、decr 操作。代码通常会这样来编写:
    
		// 库存存在则加一
        if(redisService.get(prefix, key, Integer.class) != null){
            redisService.incr(prefix, key);
        }

    场景2:对访问频次进行限流,我们可以通过redis简单实现:
    
        // 首先获取当前访问频次
		Integer count = redisService.get(prefix, key, Integer.class);
        // 如果频次为空,则设置访问次数为1
		if (count == null) {
            redisService.set(prefix, key, 1);
        } else if (count < checkFrequencyCount) {
            // 如果频次小于限制,则设置访问次数加1
            redisService.incr(prefix, key);
        } else {
			// 如果频次超过限制,则限流
            throw new AppException("访问频次过高,请稍候再试");
        }

    两种场景编码看似都没有问题,但实际运行中却发现redis中有一些key变成了永不过期的key,而且值不正确。
    原因是: 因为redis的incr操作,当key不存在时, 会生成这个key并将值初始化为0, 并且默认设置key的有效时间为永久。
    解决方案
    1.优化Java代码,例如场景2。不论这个key是否存在都先加一,然后判断其过期时间是否为永不过期,如果是永不过期则说明是新生成的key,给它设置过期时间即可,如果非永不过期则无需操作。最后再判断一下是否值已经大于访问频次了,是则限流。
    
		long count = redisService.incr(prefix, key);
		// 判断必须放在后面,否则key没有过期时间永远无法清除
        long expire = redisService.ttl(prefix, key);
        if (expire == -1) {
            redisService.setExpire(prefix, key, accessExpireSecond);
        }
        if (count > checkFrequencyCount) {
            throw new AppException("访问频次过高,请稍候再试");
        }

    2.使用lua脚本执行,保证原子性。
    脚本updateStore.lua
    
--- 获取key
local key = KEYS[1]
--- 获取参数:incr、decr
local action = ARGV[1]
--- 如果key存在,再执行增加或减少的操作
if redis.call('exists', key) == 1 
then redis.call(action, key)
    return true
end 
return false

    配置LuaConfiguration.java
    
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

@Configuration
public class LuaConfiguration {
    @Bean(name = "update")
    public DefaultRedisScript<Boolean> redisScript() {
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/updateStore.lua")));
        redisScript.setResultType(Boolean.class);
        return redisScript;
    }
}

    使用方法:
    
    @Resource(name = "update")
    private DefaultRedisScript<Boolean> redisScript;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
	// 执行脚本并传参
	Boolean result = stringRedisTemplate.execute(redisScript, Arrays.asList(stockPrefix.getPrefix() + key), "incr");

    到此这篇关于Redis增减库存避坑的实现的文章就介绍到这了,更多相关Redis增减库存内容请搜索电脑手机教程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持电脑手机教程网!