聊聊电商商品详情页面吧
对于商品详情页我想只要看过淘宝的都不会陌生,业务场景就不介绍了;
第一版
客户端上送商品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;
}