电商网站如何进行库存同步处理Redis+Lua

本文详述电商网站库存模块设计,涵盖库存表结构、操作记录、库存调整流程及Redis+Lua原子性保证,深入解析库存检查、调整及分布式锁机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

电商网站库存模块

库存表包含了商品的sku,商品类型,商品款号,颜色,尺码,库存数,版本号,创建时间,修改时间。

商品类型,可根据商品分为,普通商品,赠品,内卖商品,预售商品等

库存表结构

@Data
public class ProductStock extends OrderEntity<String> {

	private static final long serialVersionUID = 6324321924144806460L;

    /**
     * sku
     */
	private String sku;
	/**
	 * good type
	 */
	private GoodsType goodsType;

	/**
	 * 款号
	 */
	private String sn;

	/**
	 * 颜色
	 */
	private String color;

	/**
	 * 尺码
	 */
	private String size;
    
    /**
                * 库存
     */
    private Integer stock;

  	@Version
  	private Long version;

    private Date createdDate;
    
    private Date lastModifiedDate;
}

有库存表就有库存操作表,记录每次出库入库的日志。

库存操作记录表

@Data
public class ProductStockLog {

	private static final long serialVersionUID = -5035787394251728152L;

	/**
	 * 类型
	 */
	public enum Type {

		/** 入库 */
		stockIn,

		/** 出库 */
		stockOut
	}

	/** 类型 */
	private ProductStockLog.Type type;
	
    private String sku;

	private GoodsType goodsType;

	/** 入库数量 */
	private Integer inQuantity;

	/** 出库数量 */
	private Integer outQuantity;

	/** 当前库存 */
	private Integer stock;

	/** 操作员 */
	private String operator;

	/** 备注 */
	private String memo;

	/** 商品 */
	private String productName;

	@Version
	private Long version;

    private Date createdDate;
    
    private Date lastModifiedDate;
}

基础的实体创建好了,那就需要具体的操作库存了,库存的扣减一般分为下单扣库存支付扣库存等,可根据项目需要在合适的场景下进行库存操作。

首先下单的时候,肯定需要先检查库存。

检查库存

通过sku和商品的类型查询库存,返回Integer类型

    public BaseResponse<Integer> getStockNumBySku(String sku,GoodsType type) {
        try {
            String key = stockRequest.getType().name() + "_" + stockRequest.getSku();
            log.info("init key:{}", key);
            Long stock = redisStockService.getStock(key, 60 * 60, (IStockCallback) productStockService);
            if (stock == null || stock.equals(UNINITIALIZED_STOCK)) {
                log.info("没有该商品库存,请先维护商品库存数据!");
                return BaseResponse.exceptionResponse("500", "没有该商品库存,请先维护商品库存数据!");
            } else {
                return BaseResponse.successResponse(Integer.parseInt(String.valueOf(stock)));
            }
        } catch (Exception e) {
            log.error("服务器端查询库存失败", e);
            return BaseResponse.exceptionResponse("500", "服务器端查询库存失败");
        }
    }

通过redis + lua 保证库存原子性操作。
检查库存完毕后,就是调整库存了,一般情况下并发都是发生在扣库存的场景下,我们先创建一个库存调整请求类

@Data
@ApiModel(description = "调整库存请求类")
public class AdjustStockRequest {

    @ApiModelProperty(value = "sku", name = "sku", example = "sku")
    private String sku;

    @ApiModelProperty(value = "goodsType")
    private GoodsType goodsType;

    @ApiModelProperty(value = "stock", name = "增、减的库存量(负数表示减库存)", example = "0")
    private Integer stock;

    @ApiModelProperty(value = "商品SKU名称", name = "productName", example = "LV")
    private String productName;

}

以及库存操作状态的枚举类型

public enum StockOperationStatEnum {
    SUCCESS(1, "库存操作成功"),
    STOCK_UNLIMITED(-1,"库存不限"),
    STOCK_NOT_ENOUGH(-2, "库存不足"),
    STOCK_UNINITIALIZED(-3, "库存未初始化"),
	FAILED(-4, "库存操作失败");
	
    private long state;
    private String stateInfo;

    StockOperationStatEnum(long state, String stateInfo) {
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public long getState() {
        return state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public static StockOperationStatEnum stateOf(long index){
        for (StockOperationStatEnum state : values()){
            if (state.getState() == index){
                return state;
            }
        }
        return null;
    }
}

调整库存

    public BaseResponse<Boolean> adjustStock(@RequestBody AdjustStockRequest stockRequest) {
       	// TODO 省略了参数的非空校验
        String key = stockRequest.getGoodsType() + "_" + stockRequest.getSku();
        StockOperationStatEnum resultEnum = productStockService.adjustStock(key, stockRequest.getStock());
        if (resultEnum == StockOperationStatEnum.SUCCESS) {
            ProductStockLog productStockLog = new ProductStockLog();
            productStockLog.setSku(stockRequest.getSku());
            productStockLog.setGoodsType(stockRequest.getGoodsType());
            productStockLog.setType(stockRequest.getStock() > 0 ? ProductStockLog.Type.stockIn : ProductStockLog.Type.stockOut);
            productStockLog.setInQuantity(stockRequest.getStock() > 0 ? stockRequest.getStock() : 0);
            productStockLog.setOutQuantity(stockRequest.getStock() < 0 ? Math.abs(stockRequest.getStock()) : 0);
            Long stock = redisStockService.getStock(key, 60 * 60, (IStockCallback) productStockService);
            if (stock == null || stock.equals(UNINITIALIZED_STOCK)) {
                stock = Long.valueOf(0);
            }
            //当前库存
            productStockLog.setStock(Integer.parseInt(String.valueOf(stock)));
            productStockLog.setOperator("customer");
            productStockLog.setProductName(stockRequest.getProductName());
            productStockLogService.saveProductStockLog(productStockLog);
            //  调整库存后更改商品假库存
            ProductStockVo productStockVo = new ProductStockVo();
            productStockVo.setStock(Integer.parseInt(String.valueOf(stock)));
            productStockVo.setGoodsType(stockRequest.getGoodsType());
            productStockVo.setSku(stockRequest.getSku());
            stockSendGoods.send(GsonUtil.toJson(productStockVo));
            return BaseResponse.successResponse(true);
        }
        return BaseResponse.exceptionResponse("500",false);
    }

整个库存的操作我们都是放到了redis中,通过redis+lua保证库存操作的原子性,最终在完成库存调整后,通过ProductStockVo 对象,将库存调整的信息通过MQ同步到我们库存的WMS系统。

Redis+Lua

Lua 嵌入 Redis 优势:

  1. 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
  2. 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
  3. 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
@Service
@Slf4j
public class RedisStockService {

    /**
     * Redis 客户端
     */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public static final String REDIS_STOCK_KEY_PREFIX = "redis_key:stock:";
    
    public static final String REDIS_INIT_STOCK_KEY_PREFIX = "redis_key:stock_init:";
    /**
     * 库存未初始化
     */
    public static final Long UNINITIALIZED_STOCK = -3L;

    /**
     * 执行扣库存的脚本
     */
    private 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 (num < 0) then");
        sb.append("    	  if (stock >= math.abs(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 redis.call('incrby', KEYS[1], 0 + num);");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
        log.info("init stock_lua script:{}", STOCK_LUA);
    }

    /**
     * @param key           库存key
     * @param expire        库存有效时间,单位秒
     * @param num           扣减数量
     * @param stockCallback 初始化库存回调函数
     * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存
     */
    public Long adjustStock(String key, long expire, int num, IStockCallback stockCallback) {
        if (StringUtils.isEmpty(key)) {
            key = "";
        }
        if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
            key = REDIS_STOCK_KEY_PREFIX + key;
        }

        //redis lua脚本修改库存
        long stock = this.adjustStock(key, num);
        // 如果没有初始库存
        if (stock == UNINITIALIZED_STOCK) {
            RedisLock redisLock = new RedisLock(redisTemplate, key);
            try {
                // 获取锁
                if (redisLock.tryLock()) {
                    // 双重验证,避免并发时重复回源到数据库
                    stock = this.adjustStock(key, num);
                    if (stock == UNINITIALIZED_STOCK) {
                        // 获取初DB库存
                        final Long initStock = stockCallback.getDataBaseStock(key);
                        if (initStock != null && initStock >= 0) {
                            // 将库存设置到redis
                            redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
                            // 调整库存的操作
                            stock = this.adjustStock(key, num);
                        }
                    }
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                redisLock.unlock();
            }
        }
        return stock;
    }

    /**
     * 获取库存
     *
     * @param key 库存key
     * @return -1:不限库存; 大于等于0:剩余库存
     */
    public Long getStock(String key, long expire, IStockCallback stockCallback) {
        if (StringUtils.isEmpty(key)) {
            key = "";
        }
        if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
            key = REDIS_STOCK_KEY_PREFIX + key;
        }
        Integer stockCache = (Integer) redisTemplate.opsForValue().get(key);
        Long stock = null;
        if (stockCache == null || stockCache.longValue() == UNINITIALIZED_STOCK) {
            RedisLock redisLock = new RedisLock(redisTemplate, key);
            try {
                // 获取锁
                if (redisLock.tryLock()) {
                    if (stockCache == null || stockCache.longValue() == UNINITIALIZED_STOCK) {
                        // 获取DB库存
                        final Long initStock = stockCallback.getDataBaseStock(key);
                        if (initStock != null && initStock >= 0) {
                            // 将库存设置到redis
                            redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
                        }
                        stock = initStock;
                    }
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                redisLock.unlock();
            }
        } else {
            stock = stockCache.longValue();
        }

        return stock;
    }

    public void setRedisKeyValue(String key, Object stock) {
        if (StringUtils.isEmpty(key)) {
            key = "";
        }
        if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
            key = REDIS_STOCK_KEY_PREFIX + key;
        }
        redisTemplate.opsForValue().set(key, stock, 60 * 60, TimeUnit.SECONDS);
    }

    /**
     * 扣库存
     *
     * @param key 库存key
     * @param num 扣减库存数量
     * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
     */
    private Long adjustStock(String key, int num) {
        if (StringUtils.isEmpty(key)) {
            key = "";
        }
        if (StringUtils.countMatches(key, REDIS_STOCK_KEY_PREFIX) <= 0) {
            key = REDIS_STOCK_KEY_PREFIX + key;
        }
        // 脚本里的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;
    }

}

getStock获取库存的时候,如果是首次获取,库存并没存在缓存中,或者库存尚未初始化时,通过redis锁防止分布式场景中并发操作,DB获取库存放到redis中。
adjustStock如果调整库存失败,将会通过回调函数stockCallback,初始化当前库存,并且再将库存设置到redis中。

回调函数初始化当前库存

public interface IStockCallback {

	/**
	 * 获取库存
	 * @return
	 */
	Long getDataBaseStock(String sku);
}

分布式环境下,我们需要一个分布式锁来控制只能有一个服务去初始化库存,因此在对库存进行操作的时候,优先判断redis是否获取到了锁tryLock

redis分布式锁

/**
 * Redis分布式锁
 * 使用 SET resource-name anystring NX EX max-lock-time 实现
 * EX seconds — 以秒为单位设置 key 的过期时间;
 * PX milliseconds — 以毫秒为单位设置 key 的过期时间;
 * NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
 * XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。
 * <p>
 * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
 * <p>
 * 客户端执行以上的命令:
 * <p>
 * 如果服务器返回 OK ,那么这个客户端获得锁。
 * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
 *
 */
@Slf4j
public class RedisLock {

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
     */
    public static final String NX = "NX";

    /**
     * seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
     */
    public static final String EX = "EX";

    /**
     * 调用set后的返回值
     */
    public static final String OK = "OK";

    /**
     * 默认请求锁的超时时间(ms 毫秒)
     */
    private static final long TIME_OUT = 100;

    /**
     * 默认锁的有效时间(s)
     */
    public static final int EXPIRE = 60;

    /**
     * 解锁的lua脚本
     */
    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
        log.debug("init UNLOCK_LUA:{}", UNLOCK_LUA);
    }

    /**
     * 锁标志对应的key
     */
    private String lockKey;

    /**
     * 记录到日志的锁标志对应的key
     */
    private String lockKeyLog = "";

    /**
     * 锁对应的值
     */
    private String lockValue;

    /**
     * 锁的有效时间(s)
     */
    private int expireTime = EXPIRE;

    /**
     * 请求锁的超时时间(ms)
     */
    private long timeOut = TIME_OUT;

    /**
     * 锁标记
     */
    private volatile boolean locked = false;

    final Random random = new Random();

    /**
     * 使用默认的锁过期时间和请求锁的超时时间
     *
     * @param redisTemplate
     * @param lockKey       锁的key(Redis的Key)
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * 使用默认的请求锁的超时时间,指定锁的过期时间
     *
     * @param redisTemplate
     * @param lockKey       锁的key(Redis的Key)
     * @param expireTime    锁的过期时间(单位:秒)
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int expireTime) {
        this(redisTemplate, lockKey);
        this.expireTime = expireTime;
    }

    /**
     * 使用默认的锁的过期时间,指定请求锁的超时时间
     *
     * @param redisTemplate
     * @param lockKey       锁的key(Redis的Key)
     * @param timeOut       请求锁的超时时间(单位:毫秒)
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, long timeOut) {
        this(redisTemplate, lockKey);
        this.timeOut = timeOut;
    }

    /**
     * 锁的过期时间和请求锁的超时时间都是用指定的值
     *
     * @param redisTemplate
     * @param lockKey       锁的key(Redis的Key)
     * @param expireTime    锁的过期时间(单位:秒)
     * @param timeOut       请求锁的超时时间(单位:毫秒)
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int expireTime, long timeOut) {
        this(redisTemplate, lockKey, expireTime);
        this.timeOut = timeOut;
    }

    /**
     * 自旋锁
     * 尝试获取锁 超时返回
     *
     * @return
     */
    public boolean tryLock() {
        // 生成随机key
        lockValue = UUID.randomUUID().toString();
        // 请求锁超时时间,纳秒
        long timeout = timeOut * 1000000;
        // 系统当前时间,纳秒
        long nowTime = System.nanoTime();
        while ((System.nanoTime() - nowTime) < timeout) {
            if (OK.equalsIgnoreCase(this.set(lockKey, lockValue, expireTime))) {
                locked = true;
                // 上锁成功结束请求
                log.debug("获取redis锁:key{},value:{},expireTime:{}", lockKey, lockValue, expireTime);
                return locked;
            }

            // 每次请求等待一段时间
            seleep(10, 50000);
        }
        return locked;
    }

    /**
     * 尝试获取锁 立即返回
     *
     * @return 是否成功获得锁
     */
    public boolean lock() {
        lockValue = UUID.randomUUID().toString();
        //不存在则添加 且设置过期时间(单位ms)
        String result = set(lockKey, lockValue, expireTime);
        locked = OK.equalsIgnoreCase(result);
        return locked;
    }

    /**
     * 以阻塞方式的获取锁
     *
     * @return 是否成功获得锁
     */
    public boolean lockBlock() {
        lockValue = UUID.randomUUID().toString();
        while (true) {
            //不存在则添加 且设置过期时间(单位ms)
            String result = set(lockKey, lockValue, expireTime);
            if (OK.equalsIgnoreCase(result)) {
                locked = true;
                return locked;
            }

            // 每次请求等待一段时间
            seleep(10, 50000);
        }
    }

    /**
     * 解锁
     * <p>
     * 可以通过以下修改,让这个锁实现更健壮:
     * <p>
     * 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
     * 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
     * 这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。
     */
    public Boolean unlock() {
        // 只有加锁成功并且锁还有效才去释放锁
        // 只有加锁成功并且锁还有效才去释放锁
        if (locked) {
            return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    Long result = 0L;

                    List<String> keys = new ArrayList<>();
                    keys.add(lockKey);
                    List<String> values = new ArrayList<>();
                    values.add(lockValue);

                    // 集群模式
                    if (nativeConnection instanceof JedisCluster) {
                        result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
                    }

                    // 单机模式
                    if (nativeConnection instanceof Jedis) {
                        result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
                    }

                    if (result == 0 && !StringUtils.isEmpty(lockKeyLog)) {
                        logger.info("Redis分布式锁,解锁{}失败!解锁时间:{}", lockKeyLog, System.currentTimeMillis());
                    }

                    locked = result == 0;
                    return result == 1;
                }
            });
        }

        return true;
    }

    /**
     * 获取锁状态
     *
     * @return
     * @Title: isLock
     */
    public boolean isLock() {

        return locked;
    }

    /**
     * 重写redisTemplate的set方法
     * <p>
     * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
     * <p>
     * 客户端执行以上的命令:
     * <p>
     * 如果服务器返回 OK ,那么这个客户端获得锁。
     * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
     *
     * @param key     锁的Key
     * @param value   锁里面的值
     * @param seconds 过去时间(秒)
     * @return
     */
    private String set(final String key, final String value, final long seconds) {
        Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空");
        return (String) redisTemplate.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                String result = null;
                if (nativeConnection instanceof JedisCommands) {
                    result = ((JedisCommands) nativeConnection).set(key, value, NX, EX, seconds);
                }

                if (!StringUtils.isEmpty(lockKeyLog) && !StringUtils.isEmpty(result)) {
                    logger.info("获取锁{}的时间:{}", lockKeyLog, System.currentTimeMillis());
                }

                return result;
            }
        });
    }

    /**
     * @param millis 毫秒
     * @param nanos  纳秒
     * @Title: seleep
     * @Description: 线程等待时间
     */
    private void seleep(long millis, int nanos) {
        try {
            Thread.sleep(millis, random.nextInt(nanos));
        } catch (InterruptedException e) {
            logger.info("获取分布式锁休眠被中断:", e);
        }
    }

    public String getLockKeyLog() {
        return lockKeyLog;
    }

    public void setLockKeyLog(String lockKeyLog) {
        this.lockKeyLog = lockKeyLog;
    }

    public int getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(int expireTime) {
        this.expireTime = expireTime;
    }

    public long getTimeOut() {
        return timeOut;
    }

    public void setTimeOut(long timeOut) {
        this.timeOut = timeOut;
    }

}

这样就大功告成了吗?其实细心的同学已经发现了在我们代码中有这么一段Integer stockCache = (Integer) redisTemplate.opsForValue().get(key);,那是因为我在向redis放库存的时候存放的是Long类型,所以在这里需要强转一下,使用RedisTemplate的时候默认的序列化反序列方式为JdkSerializationRedisSerializer,如果我们只是存一下参数倒还无所谓,如果存的是序列化的对象,那么反序列化拿到的key就不再是我们需要的key了。当然在这个项目中,如果我们约定了redis,key的类型是Integer,那么get的时候就不需要转换。但是我们在set的时候是用的Long类型,那么通过Integer强转的时候,肯定会报转换错误。所以需要我们自定义一下redis序列化方式,指定redis 的key和value使用什么类型。

自定义序列化方式

@Configuration
public class RedisConfig {

    /**
     * 重写Redis序列化方式,使用Json方式:
     * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。
     * Spring Data JPA为我们提供了下面的Serializer:
     * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
     * 在此我们将自己配置RedisTemplate并定义Serializer。
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 设置值(value)的序列化采用Jackson2JsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 设置键(key)的序列化采用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

ok。这样在redis放值取值时,如果类型不一致,进行转换的时候就不会出错了。

当然这仅仅是商城端扣减库存的操作,在上边的代码中我们并没有太多的针对库存系统(类似WMS)同步的问题,只是在调整的时候通过MQ将商城端的库存同步过去了。如果MQ失败了会怎样,岂不是从一开始就错下去了。
其实在实际项目中,这种这种商城端库存推送我们一般称之为“增量库存”,增量库存失败的情况下,系统自动调用同步全量库存。同时为了尽可能保证完全商城商品库存和仓库商品库存同步,一般需要系统定时的去获取仓库的全量库存,来尽可能保证仓库库存与商城库存一致。

之前看过一篇关于中台程序的电商更新文档,里边一些业务实现方案还是可以参考一下的:比如这个增量同步库存扣减失败系统库存同步处理

<think>嗯,用户之前让我详细讲解了Redis,现在要求将Redis与MySQL进行对比。我需要先回顾之前的内容,确保对比覆盖存储机制、优缺点和应用场景。用户可能想知道在不同情况下该选择哪种数据库,或者是想理解两者的根本区别。 首先,得明确Redis是内存型键存储,而MySQL是关系型数据库,基于磁盘。存储结构上,Redis支持多种数据结构,而MySQL是表格结构,支持SQL查询。性能方面,Redis肯定更快,但持久化和数据规模可能不如MySQL。 然后,用户可能关心两者的优缺点对比,尤其是在存储方面。比如Redis内存限制,而MySQL可以处理更大规模的数据,但速度较慢。事务支持方面,MySQL有ACID,而Redis的事务相对简单。 应用场景方面,用户可能想知道何Redis作为缓存,何用MySQL做主数据库。需要提到结合使用的情况,比如用Redis加速MySQL的查询。 还要注意用户可能的深层需求,比如系统架构设计中的选择考量,或者如何解决数据一致性问题。需要给出实际例子,比如电商中的库存缓存用Redis,订单处理用MySQL。 最后,确保对比清晰,结构分明,用表格或分点列出,让用户一目了然。避免技术术语过多,用简单的中文解释,必要举例说明。</think>--- ### **Redis vs MySQL 核心对比** | **特性** | **Redis** | **MySQL** | |------------------------|-----------------------------------------------|-----------------------------------------------| | **数据模型** | 键+ 多种数据结构(字符串、哈希、列表等) | 关系型表结构,支持复杂 JOIN 和 SQL 查询 | | **存储介质** | **内存为主**(持久化可选) | **磁盘为主**(通过 Buffer Pool 缓存部分数据) | | **读写性能** | **极高**(微秒级延迟,10万+ QPS) | **中等**(毫秒级延迟,数千 QPS) | | **数据规模** | 受内存限制(通常百 GB 级以内) | 支持 TB 级数据(依赖磁盘容量) | | **事务支持** | 简单事务(无 ACID,通过 Lua 脚本实现原子性) | 完整 ACID 特性(原子性、一致性、隔离性、持久性)| | **适用场景** | 缓存、实计算、消息队列 | 复杂查询、事务处理、持久化业务数据存储 | | **扩展性** | 集群模式支持水平扩展 | 主从复制、分库分表(复杂度较高) | --- ### **详细对比分析** #### 1. **数据模型与查询能力** - **Redis** - **优势**:直接操作数据结构(例如:集合求交集、有序集合排序),无需解析 SQL。 - **局限**:无法实现关联查询(如 `JOIN`),过滤条件简单(仅支持键匹配或范围查询)。 - **示例**:获取用户 `user:1000` 的姓名 → `HGET user:1000 name`。 - **MySQL** - **优势**:通过 SQL 实现复杂查询(如多表关联、聚合函数、子查询)。 - **示例**:统计订单表中每个用户的消费总额 → ```sql SELECT user_id, SUM(amount) FROM orders GROUP BY user_id; ``` --- #### 2. **存储与性能** - **Redis** - **内存存储**:数据常驻内存,读写速度极快(例如:读取字符串仅需 $O(1)$ 间复杂度)。 - **持久化代价**:RDB 快照可能阻塞线程,AOF 日志写入影响吞吐量(需权衡性能与数据安全)。 - **典型场景**:缓存热门商品信息,QPS 从 MySQL 的 1万 提升到 Redis 的 10万+。 - **MySQL** - **磁盘存储**:数据持久化到磁盘,通过 Buffer Pool 缓存热点数据(内存不足性能下降明显)。 - **索引优化**:通过 B+ 树索引加速查询(例如:主键查询 $O(\log N)$,全表扫描 $O(N)$)。 --- #### 3. **事务与一致性** - **Redis** - **弱事务**:`MULTI/EXEC` 命令仅保证命令顺序执行,不满足 ACID(无回滚机制)。 - **解决方案**:通过 Lua 脚本实现原子操作(例如:库存扣减的原子性校验)。 - **MySQL** - **强事务**:支持事务隔离级别(如 Read Committed、Repeatable Read),通过 Undo/Redo 日志保障一致性。 - **示例**:转账操作(原子性保障): ```sql BEGIN; UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A'; UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B'; COMMIT; ``` --- #### 4. **扩展性与成本** - **Redis** - **垂直扩展**:受单机内存限制,成本高(例如:64GB 内存服务器价格约 5000 元)。 - **水平扩展**:集群模式分散数据(需解决分片和数据迁移问题)。 - **MySQL** - **垂直扩展**:通过提升 CPU/内存/磁盘性能(成本可控,例如:1TB SSD 约 800 元)。 - **水平扩展**:分库分表复杂度高(需处理跨分片查询和事务)。 --- ### **协作使用建议** 1. **缓存加速** - 用 Redis 缓存 MySQL 热点数据(如商品详情),降低数据库压力。 - **注意**:需维护缓存一致性(可通过延迟双删或订阅 Binlog 同步)。 2. **实统计** - 用 Redis 计数器(`INCR`)统计页面 PV,定同步到 MySQL 持久化。 3. **削峰填谷** - 用 Redis 队列缓冲高并发请求(如秒杀订单),MySQL 异步处理。 --- ### **总结:如何选择?** - **选 Redis**:需要**低延迟、高并发、弱一致性**的场景(如缓存、排行榜)。 - **选 MySQL**:需要**复杂查询、强事务、海量数据持久化**的场景(如订单管理、财务系统)。 - **混合使用**:多数互联网系统同使用两者(Redis 作缓存层,MySQL 作持久层)。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值