秒杀系统-商品详细页多级缓存实战(二)

一、缓存方案

后台问题描述:访问数据库查询商品信息,读多写少。

/**
 * 获取商品详情信息
 *
 * @param id 产品ID
 */
public PmsProductParam getProductInfo(Long id) {
    PmsProductParam productInfo = null;
    productInfo = portalProductDao.getProductInfo(id);
    if (null == productInfo) {
        return productInfo;
    }
    FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);
    if (!ObjectUtils.isEmpty(promotion)) {
        productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount());
        productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit());
        productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice());
        productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId());
        productInfo.setFlashPromotionEndDate(promotion.getEndDate());
        productInfo.setFlashPromotionStartDate(promotion.getStartDate());
        productInfo.setFlashPromotionStatus(promotion.getStatus());
    }
    return productInfo;
}

前端静态化页面:
我们发现上节课通过我们对数据进行静态化,也是有很多问题的,比如我们商品如果过多,freemark模板一定修改之后,我们所有的商品都需要重新再次生产静态化,这个工作量实在是太大了。

1.1 引入缓存

在这里插入图片描述
传统的直接查询mysql要进行网络IO和磁盘IO。而我们查询缓存的话,数据是在内存中的,只有网络IO,而且也可以大大减少数据库的压力。因为缓存的抗并发能力是要大大优于数据库的。

二、实战-获取商品详情信息(zk分布式锁+本地缓存+Redis)

1、错误的代码

/**
 * 获取商品详情信息
 *
 * @param id 产品ID
 */
public PmsProductParam getProductInfo(Long id) {
    PmsProductParam productInfo = null;

    productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
    if (productInfo != null) {
        return productInfo;
    }
    productInfo = portalProductDao.getProductInfo(id);
    if (null == productInfo) {
        return null;
    }
    FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);
    if (!ObjectUtils.isEmpty(promotion)) {
        productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount());
        productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit());
        productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice());
        productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId());
        productInfo.setFlashPromotionEndDate(promotion.getEndDate());
        productInfo.setFlashPromotionStartDate(promotion.getStartDate());
        productInfo.setFlashPromotionStatus(promotion.getStatus());
    }
    redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
    return productInfo;
}

2.1 现在有什么问题了?

问题一:数据压缩

我们知道Redis虽然可以解决磁盘IO问题,但是网络IO问题依然是存在的。而影响网络IO的两个因素是网络带宽和传输文件大小。我们这里还需要对文件大小,也就是缓存数据的大小进行优化:压缩文件!!!

问题二:并发带来的多次访问数据库问题

在这里插入图片描述

跟我们预期只set一次redis 是有出入,为何会这样子了?并发问题导致set了多次redis缓存。

这里要使用分布式锁来解决:redis(redisson)或者zookeeper

2.2 加入分布式锁:

2.2.1 Redis实现分布式锁

<!--加入redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>

或者使用setnx

单机加锁:
在这里插入图片描述
分布式加锁:
同一时间同一个数据请求过来,比如set 100 value:(1 2 3)
在这里插入图片描述
Redis高并发分布式锁实战(Setnx、Redisson及底层源码、RedLock):库存扣减中理解分布式锁的含义【干货满满】:https://blog.csdn.net/qq_43631716/article/details/118510581

优化后代码

在这里插入图片描述

1、直接加锁版本

@Autowired
RedissonClient redission;


/**
 * 获取商品详情信息  加入redis 加入锁
 *
 * @param id 产品ID
 */
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.Lock()) {
            // DCL
            productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);  
            if (null == productInfo) {
                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);
            } 
            
        }
    } finally {
        lock.unlock();
    }
    return productInfo;
}

这样直接加锁会导致所有请求都要穿行执行,并发能力会大大下降。可以考虑使用其他方式进行优化,比如使用tryLock,尽量减少请求进入锁的逻辑。

2、tryLock尝试加锁版本

@Autowired
RedissonClient redission;

/**
 * 获取商品详情信息  加入redis 加入锁
 *
 * @param id 产品ID
 */
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 {
            // 尝试获取锁失败后,再尝试查询一次redis,但是不一定能查询到数据,可能返回null,
            // 这里优化空间就是sleep一会后重新执行该方法进行查询
            productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
            if (null == productInfo) {
				Thread.sleep(500);
				getProductInfo3(id); // 重试该方法	
			} 
        }
    } finally {
        // 判断锁是否依然存在,因为尝试获取锁失败的线程会先执行到这里,所以一定要让真正持有锁的线程去释放锁
        if (lock.isLocked()) {
            // 判断锁是否是被当前线程持有,这样才能释放锁。其他线程是无法释放锁的,会报错
            if (lock.isHeldByCurrentThread())
                lock.unlock();
        }
    }
    return productInfo;
}

2.2.2 ZK分布式锁实现

我们知道Redis是属于AP架构的,如果我们使用分布式锁的时候刚好将值写在master节点上的时候,突然挂掉了,此时从节点还没来得及同步锁数据,这样很可能会造成锁丢失或者其他一系列的问题。

而ZK是基于CP架构的,性能虽然弱与Redis,但是它写入数据后,必须有大于半数的节点返回ack才表示一个节点写入成功,这样就能保证当leader节点挂掉之后,那个被选举为新leader的从节点上是存在最新的数据的!

1、实现类

import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

/**
 * zk分布式锁
 **/
@Slf4j
public class ZKLockImpl implements ZKLock, InitializingBean {

    private final static String LOCK_ROOT_PATH = "/ZkLock";

    private Map<String, CountDownLatch> concurrentMap = new ConcurrentHashMap<>();

    private ReentrantLock lock = new ReentrantLock();

    @Autowired
    private CuratorFramework curatorFramework;

    @Override
    public boolean lock(String lockpath) {
        boolean result = false;
        String keyPath = LOCK_ROOT_PATH + lockpath;
        try {
            curatorFramework
                    .create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                    .forPath(keyPath); // 创建失败会抛出异常Exception
            result = true;
            log.info("success to acquire mutex lock for path:{}", keyPath);
        } catch (Exception e) {  // 获取锁失败的线程需要使用CountDownLatch进行阻塞等待,知道其他线程释放锁
            log.info("Thread:{};failed to acquire mutex lock for path:{}", Thread.currentThread().getName(), keyPath);
            if (!concurrentMap.containsKey(lockpath)) {
                try {
                    /*
                     * 这里考虑到高并发场景,必须保证对同一个节点加锁的线程失败后是落在同一个countdown对象上
                     * ,否则有的线程永远没有办法唤醒了
                     */
                    lock.lock();
                    //双重校验,考虑高并发问题
                    if (!concurrentMap.containsKey(lockpath)) {
                        concurrentMap.put(lockpath, new CountDownLatch(1));
                    }
                } finally {
                    lock.unlock();
                }
            }
            try {
                CountDownLatch countDownLatch = concurrentMap.get(lockpath);
                //这里为什么要判断呢?大家可以思考一下,高并发场景
                if (countDownLatch != null) {
                    countDownLatch.await();
                }
            } catch (InterruptedException e1) {
                log.info("InterruptedException message:{}", e1.getMessage());
            }
        }
        return result;
    }

    @Override
    public boolean unlock(String lockpath) {
        String keyPath = LOCK_ROOT_PATH + lockpath;
        try {
            if (curatorFramework.checkExists().forPath(keyPath) != null) {
                curatorFramework.delete().forPath(keyPath);
            }
        } catch (Exception e) {
            log.error("failed to release mutex lock");
            return false;
        }
        return true;
    }

    /**
     * 监听节点事件
     *
     * @param lockPath 加锁的路径
     */
    private void addWatcher(String lockPath) throws Exception {
        String keyPath;
        if (LOCK_ROOT_PATH.equals(lockPath)) {
            keyPath = lockPath;
        } else {
            keyPath = LOCK_ROOT_PATH + lockPath;
        }

        final PathChildrenCache cache = new PathChildrenCache(curatorFramework, keyPath, false);
        cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);

        /*
         * 添加监听器
         */
        cache.getListenable().addListener((client, event) -> {
            if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
                String oldPath = event.getData().getPath();
                log.info("oldPath delete:{},redis缓存已经更新!", oldPath);
                if (oldPath.contains(lockPath)) {
                    //TODO 释放计数器,释放锁
                    CountDownLatch countDownLatch = concurrentMap.remove(oldPath);
                    if (countDownLatch != null) {//有可能没有竞争,countdown不存在
                        countDownLatch.countDown();
                    }
                }
            }
        });
    }

    @Override
    public void afterPropertiesSet() {
        curatorFramework = curatorFramework.usingNamespace("zklock-namespace");
        //zk锁的根路径 不存在则创建
        try {
            if (curatorFramework.checkExists().forPath(LOCK_ROOT_PATH) == null) {
                curatorFramework
                        .create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(LOCK_ROOT_PATH);
            }
            //启动监听器
            addWatcher(LOCK_ROOT_PATH);
        } catch (Exception e) {
            log.error("connect zookeeper failed:{}", e.getMessage(), e);
        }
    }

}

2、寄啊锁逻辑

引入本地缓存LocalCache (解决Redis网络IO问题)

在这里插入图片描述

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.tuling.tulingmall.domain.PmsProductParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * @description: 缓存管理工具,采用LRU淘汰策略
 **/
@Slf4j
@Component
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);
    }

}

考虑使用concurrentHashMap,什么时候清楚数据,如何清除都比较麻烦,所以考虑使用guava的localCache。

优化后的代码
@Autowired
private LocalCache cache;

/*
 * zk分布式锁
 */
@Autowired
private ZKLock zkLock;
private String lockPath = "/load_db";

/**
 * 获取商品详情信息 分布式锁、 本地缓存、redis缓存
 *
 * @param id 产品ID
 */
public PmsProductParam getProductInfo3(Long id) {
    PmsProductParam productInfo = null;
    // 1、从本地缓存中查询商品信息
    productInfo = cache.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id);
    if (null != productInfo) {
        return productInfo;
    }
    // 2、从Redis中查询商品信息
    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;
    }

    // 3、本地缓存和Redis中都不存在商品信息,加锁查询数据库
    try {
        // 获取锁成功的话
        if (zkLock.lock(lockPath + "_" + id)) {
            productInfo = portalProductDao.getProductInfo(id);
            if (null == productInfo) {
                return null;
            }
            // 数据库中获取数据
            checkFlash(id, productInfo);
            log.info("set db productId:" + productInfo);
            // 数据缓存到redis
            redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo, 3600, TimeUnit.SECONDS);
            // 数据缓存到local cache
            cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo);
        } else { // 获取锁失败的话
            log.info("get redis2 productId:" + productInfo);
            // 尝试从Redis获取
            productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, PmsProductParam.class);
            if (productInfo != null) {
                // 添加到local cache
                cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id, productInfo);
            }
        }
    } finally {
        log.info("unlock :" + productInfo);
        zkLock.unlock(lockPath + "_" + id);
    }
    return productInfo;
}

private void checkFlash(Long id, PmsProductParam productInfo) {
    FlashPromotionParam promotion = flashPromotionProductDao.getFlashPromotion(id);
    if (!ObjectUtils.isEmpty(promotion)) {
        productInfo.setFlashPromotionCount(promotion.getRelation().get(0).getFlashPromotionCount());
        productInfo.setFlashPromotionLimit(promotion.getRelation().get(0).getFlashPromotionLimit());
        productInfo.setFlashPromotionPrice(promotion.getRelation().get(0).getFlashPromotionPrice());
        productInfo.setFlashPromotionRelationId(promotion.getRelation().get(0).getId());
        productInfo.setFlashPromotionEndDate(promotion.getEndDate());
        productInfo.setFlashPromotionStartDate(promotion.getStartDate());
        productInfo.setFlashPromotionStatus(promotion.getStatus());
    }
}

我们发现在高并发下增加了分布式锁可以解决刚才那个问题,但是也降低了qps,所以这儿还是要根据需求而定。

2.3 ZK分布式锁原理

在这里插入图片描述

在这里插入图片描述

zk图:
Zookeeper典型使用场景实战:分布式锁、注册中心:https://blog.csdn.net/qq_43631716/article/details/118786533
在这里插入图片描述

2.4 如何保证数据一致性

1、最终一致性方案: 设置超时时间来解决

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

2、实时一致性方案: 使用阿里的canal组件, 读取binlog并修改redis中的数据

2.5 缓存应用场景

1、访问量大、QPS高、更新频率不是很高的业务
2、数据一致性要求不高

二、缓存问题

参考文章:Redis缓存设计(key、value设计)与性能优化(缓存击穿、缓存穿透、缓存雪崩)

缓存击穿问题(热点数据单个key)

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

在这里插入图片描述
解决方案:
1、加锁:在未命中缓存时,通过加锁避免大量请求访问数据库
2、不允许过期: 物理不过期,也就是不设置过期时间。而是逻辑上定时在后台异步的更新数据。
3、采用二级缓存: L1缓存失效时间短,L2缓存失效时间长。请求优先从L1缓存获取数据,如果未命中,则加锁,保证只有一个线程去数据库中读取数据然后再更新到L1和L2中。然后其他线程依然在L2缓存获取数据。

缓存穿透问题(恶意攻击、访问不存在数据)

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

在这里插入图片描述
解决方案: 有很多种方法可以有效地解决缓存穿透问题
1、最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
在这里插入图片描述

2、另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟

public List<String> getData03() {
   List<String> result = new ArrayList<String>();
    // 从缓存读取数据
    result = getDataFromCache();
    if (result.isEmpty()) {
        synchronized (lock) {
        //双重判断,第二个以及之后的请求不必去找数据库,直接命中缓存
            // 查询缓存
            result = getDataFromCache();
            if (result.isEmpty()) {
                // 从数据库查询数据
                result = getDataFromDB();
                // 将查询到的数据写入缓存
                setDataToCache(result);
            }
        }
    }
    return result;
}
static Lock reenLock = new ReentrantLock();
 
public List<String> getData04() throws InterruptedException {
    List<String> result = new ArrayList<String>();
    // 从缓存读取数据
    result = getDataFromCache();
    if (result.isEmpty()) {
        if (reenLock.tryLock()) { // 如果是分布式,要用redisson分布式锁
            try {
                System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
                // 从数据库查询数据
                result = getDataFromDB();
                // 将查询到的数据写入缓存
                setDataToCache(result);
            } finally {
                reenLock.unlock();// 释放锁
            }

        } else {
            result = getDataFromCache();// 先查一下缓存
            if (result.isEmpty()) {
                System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
                // 这里可以自旋或者sleep
                Thread.sleep(100);// 小憩一会儿
                return getData04();// 重试
            }
        }
    }
    return result;
}

缓存雪崩(同一时间失效,并发量大)

存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

在这里插入图片描述

在这里插入图片描述
解决方案:

1、缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。 这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
2、事前这种方案就是在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。
3、事中使用 Hystrix或者Sentinel进行限流 & 降级,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
4、事后:开启Redis持久化机制,尽快恢复缓存集群

缓存和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。

答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。

我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

推荐做法:
1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

3、【推荐】如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。

/**
 *	 ====================  读锁:查询库存 =======================
 */
@RequestMapping("/get_stock")
public String getStock(@RequestParam("clientId") Long clientId) {

    String lockKey = "product_stock_101";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock(lockKey);
    RLock rLock = readWriteLock.readLock();
    System.out.println("获取读锁成功. ClientId: " + clientId);

    // 获取到redisson锁对象
    try {
        rLock.lock();

        // ======== 扣减库存业务员开始 ============
        // 从redis获取库存数量
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        return String.valueOf(stock);
        // ======== 扣减库存业务员结束 ============
    } finally { // 防止异常导致锁无法释放!!!
        // ============= 释放redisson锁 ==========
        rLock.unlock();
        System.out.println("释放读锁成功. ClientId: " + clientId);
    }
}

加了写锁之后,相当于只有一个lockKey(“product_stock_101”),使用setnx设置之后,只能有一个线程持有这个lockKey完成修改操作,相当于串行执行修改的代码。

/**
 *	 ====================  写锁:修改库存 =======================
 */
@RequestMapping("/update_stock")
public String updateStock(@RequestParam("clientId") Long clientId) {

    String lockKey = "product_stock_101";
    RReadWriteLock readWriteLock = redisson.getReadWriteLock(lockKey);
    WLock wLock = readWriteLock.writeLock();
    System.out.println("获取写锁成功. ClientId: " + clientId);

    // 获取到redisson锁对象
    try {
        wLock .lock();

        // ======== 扣减库存业务员开始 ============
        System.out.println("修改商品的库存为6....");
        stringRedisTemplate.delete("stock");
        // ======== 扣减库存业务员结束 ============
    } finally { // 防止异常导致锁无法释放!!!
        // ============= 释放redisson锁 ==========
        wLock .unlock();
        System.out.println("释放写锁成功. ClientId: " + clientId);
    }

    return "end";
}

4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

此时应用程序不用对缓存做任何操作。因为通过监听数据库的binlog,会自动的去更新缓存。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值