Entrevistador: ¿Cómo diseñar la deducción de inventario del comercio electrónico? ¿Cómo prevenir la sobreventa?

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

solución

  • Utilice una base de datos mysql, utilice un campo para almacenar el inventario y actualice este campo cada vez que se deduzca el inventario.

  • La base de datos todavía se usa, pero el inventario se divide en múltiples registros y se almacena en múltiples registros. Cuando se deduce el inventario, se enruta. Esto aumenta la cantidad de concurrencia, pero aún así no puede evitar una gran cantidad de accesos a la base de datos. para actualizar el inventario.

  • Coloque el inventario en redis y utilice la función incrby de redis para deducir el inventario.


analizar


El primer y segundo método anteriores se basan en datos para deducir el inventario.


Basado en el inventario de pedidos de la base de datos

En la primera forma, todas las solicitudes esperarán el candado aquí, y adquirir el candado debe deducir el inventario. Se puede usar cuando la concurrencia no es alta, pero una vez que la concurrencia es grande, una gran cantidad de solicitudes se bloquearán aquí, lo que provocará que se agote el tiempo de espera de la solicitud y luego todo el sistema caerá en una avalancha y accederá con frecuencia a la base de datos. y ocupan una gran cantidad de recursos de la base de datos, por lo que este método no es aplicable en caso de alta concurrencia.


Inventario múltiple basado en base de datos

El segundo método es en realidad una versión optimizada del primer método, que aumenta la cantidad de concurrencia hasta cierto punto, pero aún requiere una gran cantidad de operaciones de actualización de la base de datos para ocupar una gran cantidad de recursos de la base de datos.

Todavía existen algunos problemas en la deducción del inventario basado en la base de datos:

  • En el método de deducción de inventario de la base de datos, la operación de deducción de inventario debe ejecutarse en una sola declaración y no se puede seleccionar primero y luego actualizar, por lo que se producirá una sobrededucción en concurrencia. como:

update number set x=x-1 where x > 0
  • MySQL en sí tiene problemas con el rendimiento del procesamiento de alta concurrencia. En términos generales, el rendimiento de procesamiento de MySQL aumentará con el aumento de subprocesos concurrentes, pero después de alcanzar un cierto grado de concurrencia, habrá un punto de inflexión obvio y luego disminuirá todos los manera, y eventualmente incluso peor que el rendimiento de un solo hilo.

  • Cuando la reducción de inventario y la alta concurrencia se combinan, debido a que el número de operaciones de inventario está en la misma fila, habrá un problema de competencia por los bloqueos de fila de InnoDB, lo que provocará una espera mutua o incluso un punto muerto, lo que reducirá en gran medida el rendimiento de procesamiento de MySQL. y eventualmente conduce a que se produjo una excepción de tiempo de espera en la página de inicio.


basado en redis

En respuesta a los problemas anteriores, tenemos una tercera solución, que es colocar el inventario en el caché y usar la función incrby de redis para deducir el inventario, lo que resuelve los problemas de sobresuscripción y rendimiento. Pero una vez que se pierde el caché, se debe considerar un plan de recuperación. Por ejemplo, cuando el sistema de lotería deduce el inventario de premios, el inventario inicial = el inventario total - la cantidad de recompensas que se han emitido. Sin embargo, si el premio se emite de forma asincrónica, debe esperar hasta que se consuma el mensaje MQ antes reiniciar Redis para inicializar el inventario, de lo contrario habrá inconsistencias en el inventario.

La implementación específica de la deducción de inventario basada en redis.

  • Usamos el script lua de redis para realizar la deducción de inventario.

  • Dado que es un entorno distribuido, se necesita un bloqueo distribuido para controlar solo un servicio para inicializar el inventario.

  • Es necesario proporcionar una función de devolución de llamada para llamar a esta función y obtener el inventario inicializado al inicializar el inventario.

Inicialice la función de devolución de llamada de acciones (IStockCallback)

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


Servicio de Deducción de Inventario (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);  
    }  
}

más buenos artículos

  1. Serie Java de alta concurrencia (34 artículos en total)

  2. Serie maestra de MySQL (27 artículos en total)

  3. Serie maestra de Maven (10 artículos en total)

  4. Serie Mybatis (12 artículos en total)

  5. Hable sobre implementaciones comunes de coherencia de base de datos y caché.

  6. La idempotencia de la interfaz es tan importante, ¿qué es? ¿Cómo lograrlo?

  7. Los genéricos, un poco difíciles, confundirán a mucha gente, ¡eso es porque no leíste este artículo!

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

Supongo que te gusta

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