电商商品详情页设计

聊聊电商商品详情页面吧
对于商品详情页我想只要看过淘宝的都不会陌生,业务场景就不介绍了;

第一版

客户端上送商品ID,根据商品ID去数据库查询需要展示的数据返回前端;
这么做也没错,也能实现对应功能;但压测结果灰常不理想
缺点很明显,数据库压力过大等问题;

 public PmsProductParam getProductInfo1(Long id) {
        PmsProductParam productInfo = portalProductDao.getProductInfo(id);
        if (null == productInfo) {
            return null;
        }
        FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);
        if (!ObjectUtils.isEmpty(promotion)) {
         //TODO 业务逻辑
        }
        return productInfo;
    }

第二版

第一版的缺点是数据库大压力过大,那怎么减少数据压力呢?嗯…redis,加缓存层搞定该问题;

大概流程就是
请求进来先去redis判断商品数据是否存在,如果redis能获取对应数据直接返回;如果redis获取不到该数据,去数据库查询将获取的数据存redis返回;
看上去这个方案灰常完美?代码走起…

 public PmsProductParam getProductInfo2(Long id) {
        PmsProductParam productInfo = null;
        //从缓存Redis里找
        productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
        if (null != productInfo) {
            return productInfo;
        }
        productInfo = portalProductDao.getProductInfo(id);
        System.out.println("我被执行了");
        if (null == productInfo) {
            log.warn("没有查询到商品信息,id:" + id);
            return null;
        }
        checkFlash(id, productInfo);
        redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
        return productInfo;
    }

理想情况是查询一次数据库,将数据存缓存中;下次请求直接访问缓存;然鹅现实不是这样…
压测结果显示并不是只有一次去数据库查询。

貌似和预想的结果不一样?why…并发问题!!!
当第一个线程请求发现redis没有数据,去数据库查询的同时第二个请求进来发现redis依旧没有数据再次去数据库查询循环此场景,导致多次访问数据库。
问题1:数据一致性问题,如果我后端修改了上商品数据,缓存数据已在redis中,数据还会修改吗?不会…
解决方案:
1:后端维护商品的时将redis对应数据删了;
2:设置超时时间

redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);

数据一致性又分为最终一致性和强一致性,两种方案都是最终一致性解决方案。
问题2:并发问题
解决方案:
分布式锁,常用的方案有redis的setnx,redisson,zk等…为什么不能用java对象锁?你品你细品…

第三版

基于以上代码bug,我们使用redisson解决该问题

  public PmsProductParam getProductInfo3(Long id) {
        PmsProductParam productInfo = null;
        //从缓存Redis里找
        productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
        if (null != productInfo) {
            return productInfo;
        }
        RLock lock = redission.getLock(lockPath + id);
        try {
            if (lock.tryLock()) {
                productInfo = portalProductDao.getProductInfo(id);
                if (null == productInfo) {
                    log.warn("没有查询到商品信息,id:" + id);
                    return null;
                }
                checkFlash(id, productInfo);
                redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
            } else {
                productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
            }
        } finally {
            //释放锁
            if (lock.isLocked()) {
                if (lock.isHeldByCurrentThread())
                    lock.unlock();
            }
        }
        return productInfo;
    }

本以为代码写到这就完美结束了,然鹅还是有问题…

经过压测发现程序并没有走else分支,why??lock.tryLock加锁会有一个锁的等待过程,当一个请求进来,执行lock.tryLock()发现没有获取锁,会直接去等待队列导致else分支没执行。将程序中 lock.tryLock()–>lock.tryLock(0,5,TimeUnit.SECONDS); 当请求进来发现没有获取锁不进等待队列就走else分支了。

无论数据是mysql获取还是redis获取都会占用io,为了减少网络开销最终方案是将商品信息存本地map中,gava搞起来

其实这么设计也会出现数据一致性问题,可以通过zk节点监听搞定;鱼与熊掌不能兼得,根据个人业务场景决定吧。

public class LocalCache {

    private Cache<String,PmsProductParam> localCache = null;

    @PostConstruct
    private void init(){
        localCache = CacheBuilder.newBuilder()
                //设置本地缓存容器的初始容量
                .initialCapacity(10)
                //设置本地缓存的最大容量
                .maximumSize(500)
                //设置写缓存后多少秒过期
                .expireAfterWrite(60, TimeUnit.SECONDS).build();
    }


    public void setLocalCache(String key,PmsProductParam object){
        localCache.put(key,object);
    }

    public PmsProductParam get(String key){
       return localCache.getIfPresent(key);
    }

}


public PmsProductParam getProductInfo4(Long id) {
        PmsProductParam productInfo = null;
        productInfo = cache.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id);
        if (null != productInfo) {
            return productInfo;
        }
        productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
        if (productInfo != null) {
            log.info("get redis productId:" + productInfo);
            cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo);
            return productInfo;
        }
        RLock lock = redission.getLock(lockPath + id);
        try {
            if (lock.tryLock()) {
                productInfo = portalProductDao.getProductInfo(id);
                if (null == productInfo) {
                    return null;
                }
                checkFlash(id, productInfo);
                log.info("set db productId:" + productInfo);
                redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
                cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo);
            } else {
                getProductInfo4(id);
            }
        } finally {
            if (lock.isLocked()) {
                if (lock.isHeldByCurrentThread())
                    lock.unlock();
            }
        }
        return productInfo;
    }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值