Entrevistador: Como projetar a dedução do estoque de comércio eletrônico? Como evitar sobrevenda?

 
  
 
  
您好,我是路人,更多优质文章见个人博客:http://itsoku.com

solução

  • Use um banco de dados mysql, use um campo para armazenar o estoque e atualize esse campo sempre que o estoque for deduzido.

  • O banco de dados ainda é usado, mas o inventário é dividido em vários registros e armazenado em vários registros. Quando o inventário é deduzido, ele é roteado. Isso aumenta a quantidade de simultaneidade, mas ainda não pode evitar um grande número de acessos ao banco de dados para atualizar o inventário.

  • Coloque o inventário no redis e use o recurso incrby do redis para deduzir o inventário.


analisar


O primeiro e o segundo métodos acima são baseados em dados para deduzir o estoque.


Com base no inventário de pedidos do banco de dados

Na primeira forma, todas as solicitações aguardarão o bloqueio aqui, e adquirindo o bloqueio terá que descontar o estoque. Pode ser usado quando a simultaneidade não é alta, mas quando a simultaneidade é grande, um grande número de solicitações será bloqueado aqui, fazendo com que a solicitação expire e então todo o sistema entrará em avalanche; e acessará frequentemente o banco de dados e ocupam uma grande quantidade de recursos de banco de dados, portanto, este método não é aplicável no caso de alta simultaneidade.


Multi-inventário baseado em banco de dados

O segundo método é na verdade uma versão otimizada do primeiro método, que aumenta a quantidade de simultaneidade até certo ponto, mas ainda requer um grande número de operações de atualização de banco de dados para ocupar uma grande quantidade de recursos de banco de dados.

Ainda existem alguns problemas na dedução do estoque com base no banco de dados:

  • Na forma de dedução de estoque do banco de dados, a operação de dedução de estoque deve ser executada em uma instrução, e não é possível selecionar primeiro e depois atualizar, para que a dedução excessiva ocorra sob simultaneidade. como:

update number set x=x-1 where x > 0
  • O próprio MySQL tem problemas com desempenho de processamento de alta simultaneidade. De um modo geral, o desempenho de processamento do MySQL aumentará com o aumento de threads simultâneos, mas depois de atingir um certo grau de simultaneidade, haverá um ponto de inflexão óbvio e, em seguida, diminuirá todos os maneira e, eventualmente, ainda pior do que o desempenho de thread único.

  • Quando a redução de estoque e a alta simultaneidade se encontram, porque o número de operações de inventário está na mesma linha, haverá um problema de competição por bloqueios de linha do InnoDB, resultando em espera mútua ou mesmo impasse, o que reduz bastante o desempenho de processamento do MySQL, e eventualmente leva a Ocorreu uma exceção de tempo limite na página front-end.


baseado em redis

Em resposta aos problemas acima, temos uma terceira solução, que é colocar o inventário no cache e usar o recurso incrby do redis para deduzir o inventário, o que resolve os problemas de excesso de assinaturas e desempenho. Mas uma vez perdido o cache, um plano de recuperação precisa ser considerado. Por exemplo, quando o sistema de loteria deduz o estoque de prêmios, o estoque inicial = o estoque total - o número de prêmios que foram emitidos.No entanto, se o prêmio for emitido de forma assíncrona, será necessário aguardar até que a mensagem MQ seja consumida antes reiniciar o redis para inicializar o inventário. Caso contrário, haverá inconsistências de inventário.

A implementação específica da dedução de estoque com base no redis

  • Usamos o script lua do redis para realizar a dedução do estoque

  • Por se tratar de um ambiente distribuído, é necessário um bloqueio distribuído para controlar apenas um serviço para inicializar o inventário

  • É necessário fornecer uma função de retorno de chamada para chamar esta função e obter o inventário inicializado ao inicializar o inventário

Inicialize a função de retorno de chamada de estoque (IStockCallback)

/**  
 * 获取库存回调  
 * @author yuhao.wang  
 */  
public interface IStockCallback {  
   
 /**  
  * 获取库存  
  * @return  
  */  
 int getStock();  
}


Serviço de dedução de estoque (StockService)

/**  
 * 扣库存  
 *  
 * @author yuhao.wang  
 */  
@Service  
public class StockService {  
    Logger logger = LoggerFactory.getLogger(StockService.class);  
   
    /**  
     * 不限库存  
     */  
    public static final long UNINITIALIZED_STOCK = -3L;  
   
    /**  
     * Redis 客户端  
     */  
    @Autowired  
    private RedisTemplate<String, Object> redisTemplate;  
   
    /**  
     * 执行扣库存的脚本  
     */  
    public static final String STOCK_LUA;  
   
    static {  
        /**  
         *  
         * @desc 扣减库存Lua脚本  
         * 库存(stock)-1:表示不限库存  
         * 库存(stock)0:表示没有库存  
         * 库存(stock)大于0:表示剩余库存  
         *  
         * @params 库存key  
         * @return  
         *   -3:库存未初始化  
         *   -2:库存不足  
         *   -1:不限库存  
         *   大于等于0:剩余库存(扣减之后剩余的库存)  
         *      redis缓存的库存(value)是-1表示不限库存,直接返回1  
         */  
        StringBuilder sb = new StringBuilder();  
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");  
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");  
        sb.append("    local num = tonumber(ARGV[1]);");  
        sb.append("    if (stock == -1) then");  
        sb.append("        return -1;");  
        sb.append("    end;");  
        sb.append("    if (stock >= num) then");  
        sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");  
        sb.append("    end;");  
        sb.append("    return -2;");  
        sb.append("end;");  
        sb.append("return -3;");  
        STOCK_LUA = sb.toString();  
    }  
   
    /**  
     * @param key           库存key  
     * @param expire        库存有效时间,单位秒  
     * @param num           扣减数量  
     * @param stockCallback 初始化库存回调函数  
     * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存  
     */  
    public long stock(String key, long expire, int num, IStockCallback stockCallback) {  
        long stock = stock(key, num);  
        // 初始化库存  
        if (stock == UNINITIALIZED_STOCK) {  
            RedisLock redisLock = new RedisLock(redisTemplate, key);  
            try {  
                // 获取锁  
                if (redisLock.tryLock()) {  
                    // 双重验证,避免并发时重复回源到数据库  
                    stock = stock(key, num);  
                    if (stock == UNINITIALIZED_STOCK) {  
                        // 获取初始化库存  
                        final int initStock = stockCallback.getStock();  
                        // 将库存设置到redis  
                        redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);  
                        // 调一次扣库存的操作  
                        stock = stock(key, num);  
                    }  
                }  
            } catch (Exception e) {  
                logger.error(e.getMessage(), e);  
            } finally {  
                redisLock.unlock();  
            }  
   
        }  
        return stock;  
    }  
   
    /**  
     * 加库存(还原库存)  
     *  
     * @param key    库存key  
     * @param num    库存数量  
     * @return  
     */  
    public long addStock(String key, int num) {  
   
        return addStock(key, null, num);  
    }  
   
    /**  
     * 加库存  
     *  
     * @param key    库存key  
     * @param expire 过期时间(秒)  
     * @param num    库存数量  
     * @return  
     */  
    public long addStock(String key, Long expire, int num) {  
        boolean hasKey = redisTemplate.hasKey(key);  
        // 判断key是否存在,存在就直接更新  
        if (hasKey) {  
            return redisTemplate.opsForValue().increment(key, num);  
        }  
   
        Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null");  
        RedisLock redisLock = new RedisLock(redisTemplate, key);  
        try {  
            if (redisLock.tryLock()) {  
                // 获取到锁后再次判断一下是否有key  
                hasKey = redisTemplate.hasKey(key);  
                if (!hasKey) {  
                    // 初始化库存  
                    redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);  
                }  
            }  
        } catch (Exception e) {  
            logger.error(e.getMessage(), e);  
        } finally {  
            redisLock.unlock();  
        }  
   
        return num;  
    }  
   
    /**  
     * 获取库存  
     *  
     * @param key 库存key  
     * @return -1:不限库存; 大于等于0:剩余库存  
     */  
    public int getStock(String key) {  
        Integer stock = (Integer) redisTemplate.opsForValue().get(key);  
        return stock == null ? -1 : stock;  
    }  
   
    /**  
     * 扣库存  
     *  
     * @param key 库存key  
     * @param num 扣减库存数量  
     * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】  
     */  
    private Long stock(String key, int num) {  
        // 脚本里的KEYS参数  
        List<String> keys = new ArrayList<>();  
        keys.add(key);  
        // 脚本里的ARGV参数  
        List<String> args = new ArrayList<>();  
        args.add(Integer.toString(num));  
   
        long result = redisTemplate.execute(new RedisCallback<Long>() {  
            @Override  
            public Long doInRedis(RedisConnection connection) throws DataAccessException {  
                Object nativeConnection = connection.getNativeConnection();  
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行  
                // 集群模式  
                if (nativeConnection instanceof JedisCluster) {  
                    return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);  
                }  
   
                // 单机模式  
                else if (nativeConnection instanceof Jedis) {  
                    return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);  
                }  
                return UNINITIALIZED_STOCK;  
            }  
        });  
        return result;  
    }  
   
}


transferir

/**  
 * @author yuhao.wang  
 */  
@RestController  
public class StockController {  
   
    @Autowired  
    private StockService stockService;  
   
    @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)  
    public Object stock() {  
        // 商品ID  
        long commodityId = 1;  
        // 库存ID  
        String redisKey = "redis_key:stock:" + commodityId;  
        long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));  
        return stock >= 0;  
    }  
   
    /**  
     * 获取初始的库存  
     *  
     * @return  
     */  
    private int initStock(long commodityId) {  
        // TODO 这里做一些初始化库存的操作  
        return 1000;  
    }  
   
    @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)  
    public Object getStock() {  
        // 商品ID  
        long commodityId = 1;  
        // 库存ID  
        String redisKey = "redis_key:stock:" + commodityId;  
   
        return stockService.getStock(redisKey);  
    }  
   
    @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)  
    public Object addStock() {  
        // 商品ID  
        long commodityId = 2;  
        // 库存ID  
        String redisKey = "redis_key:stock:" + commodityId;  
   
        return stockService.addStock(redisKey, 2);  
    }  
}

mais bons artigos

  1. Série Java High Concurrency (34 artigos no total)

  2. Série mestre MySql (27 artigos no total)

  3. Série mestre Maven (10 artigos no total)

  4. Série Mybatis (12 artigos no total)

  5. Fale sobre implementações comuns de consistência de banco de dados e cache

  6. A idempotência da interface é tão importante, o que é? Como conseguir isso?

  7. Os genéricos, um pouco difíceis, vão deixar muita gente confusa, isso porque você não leu esse artigo!

↓↓↓ 点击阅读原文,直达个人博客
你在看吗

Acho que você gosta

Origin blog.csdn.net/likun557/article/details/132463484
Recomendado
Clasificación