zoukankan      html  css  js  c++  java
  • SpringBoot集成Redis 一 分布式锁 与 缓存

    1.添加依赖及配置(application.yml)

    <!-- 引入redis依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    spring:
      redis:
        host: 127.0.0.1
        port: 6379
    

    2.Redis分布式锁

    Redis加锁和解锁的工具类

    @Component
    @Slf4j
    public class RedisLock {
        
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        /**
         * 加锁
         *
         * @param key   productId - 商品的唯一标志
         * @param value 当前时间+超时时间 也就是时间戳
         * @return
         */
        public boolean lock(String key, String value) {
            
            //锁不存在,未被占用,可以成功设置锁
            if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
                return true;
            }
    
            //锁存在,判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
            String currentValue = stringRedisTemplate.opsForValue().get(key);
            //如果锁过期
            if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                //获取旧锁,设置新锁
                String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
                //假如多个线程同时进入上面的if,但只有第一个线程获取的oldValue是上一个锁的currentValue,得以通过下一个if,获取锁
                if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
                    return true;
                }
            }
            return false;
        }
    
    
        /**
         * 解锁
         *
         * @param key
         * @param value
         */
        public void unlock(String key, String value) {
            try {
                String currentValue = stringRedisTemplate.opsForValue().get(key);
                if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                    stringRedisTemplate.opsForValue().getOperations().delete(key);//删除key
                }
            } catch (Exception e) {
                log.error("[Redis分布式锁] 解锁出现异常了,{}", e);
            }
        }
    }
    
    
    以上代码问题:
        1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 
        2. 当锁过期的时候,如果多个客户端同时执行 jedis.getSet() 方法,
           那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
        3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    
    
    正确代码:
    
        /**
         * 尝试获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    
        SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作。
        SET_WITH_EXPIRE_TIME:给这个key加一个过期时间的设置,具体时间由第五个参数决定
    
    
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean unLock(Jedis jedis, String lockKey, String requestId) {
    
            //获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    

    参考:https://segmentfault.com/a/1190000015058486

    分布式锁示例

    @Service
    public class SeckillServiceImpl implements SeckillService{
    
        @Autowired
        private RedisLock redisLock;
    
        private static final int TIMEOUT = 10*1000;//超时时间 10s
    
        /**
         * 活动,特价,限量100000份
         */
        static Map<String,Integer> products;//模拟商品信息表
        static Map<String,Integer> stock;//模拟库存表
        static Map<String,String> orders;//模拟下单成功用户表
        static {
            /**
             * 模拟多个表,商品信息表,库存表,秒杀成功订单表
             */
            products = new HashMap<>();
            stock = new HashMap<>();
            orders = new HashMap<>();
            products.put("123456",100000);
            stock.put("123456",100000);
        }
    
        private String queryMap(String productId){//模拟查询数据库
            return "国庆活动,皮蛋特教,限量"
                    +products.get(productId)
                    +"份,还剩:"+stock.get(productId)
                    +"份,该商品成功下单用户数:"
                    +orders.size()+"人";
        }
    
        @Override
        public String querySecKillProductInfo(String productId) {
            return this.queryMap(productId);
        }
    
        /**
         * synchronized锁方法是可以解决的,但是请求会变慢,
         * 主要是没做到细粒度控制。
         * 比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。而且这个只适合单机的情况,不适合集群
         * @param productId
         */
    
        @Override
        public void orderProductMocckDiffUser(String productId) {
    
            //加锁(不同的productId有不同的锁,达到细粒度的要求)
            long time = System.currentTimeMillis() + TIMEOUT;
            if(!redisLock.lock(productId,String.valueOf(time))){
                //未获取锁,说明有人正在进行操作
                throw new SellException(101,"很抱歉,人太多了,换个姿势再试试~~");
            }
    
            //获取锁后
            //1.查询该商品库存,为0则活动结束
            int stockNum = stock.get(productId);
            if(stockNum==0){
                throw new SellException(100,"活动结束");
            }else {
                //2.下单
                orders.put(KeyUtil.getUniqueKey(),productId);
                //3.减库存
                stockNum =stockNum-1;
                try{
                    //不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。
                    // 虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
                    Thread.sleep(100);//模拟减库存的处理时间
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                stock.put(productId,stockNum);
            }
    
            //解锁
            redisLock.unlock(productId,String.valueOf(time));
        }
    }
    

    Spring Cache + Redis 简介

    Spring Cache是一种缓存实现的通用技术,基于Spring提供的Cache框架。
    
    Spring Cache提供了本身的简单实现NoOpCacheManager、ConcurrentMapCacheManager等。
    开发者也可以通过Spring Cache,快速嵌入自己的Cache实现,如Redis等。
    
    
    SpringCache是代码级的缓存,一般是使用一个ConcurrentMap。
    也就是说实际上还是是使用JVM的内存来缓存对象的,那么肯定会造成大量的内存消耗,但是使用方便。
    
    Redis 作为一个缓存服务器,是内存级的缓存。它是使用单纯的内存来进行缓存。
    spring-boot-starter-data-redis 集成了 Spring Cache
    
    
    Spring Cache + Redis 好处:
    
        既可以很方便的缓存对象,同时用来缓存的内存的是使用redis的内存,不会消耗JVM的内存,提升了性能。
    
        当然这里Redis不是必须的,换成其他的缓存服务器一样可以,只要实现Spring的Cache类,并配置到XML里面就行了。
    

    Spring Cache + Redis 缓存

    1. 方法返回的对象加了缓存注解的,一定要实现序列化!
    
    2. springboot启动类上加上注解:@EnableCaching 
    

    @Cacheable

    第一次访问会查询内容,方法会返回一个对象,返回对象的时候,会把这个对象存储。
    下一次访问的时候,不会进去这个方法,直接从redis缓存中拿
    
    value (也可使用 cacheNames):
        可看做命名空间,表示存到哪个缓存里了。
    
    key:
        表示命名空间下缓存唯一key,使用Spring Expression Language生成。
    
    condition:
        表示在哪种情况下才缓存结果(对应的还有unless,哪种情况不缓存),同样使用SpEL
    
    
        @Cacheable(value = "models", key = "#model.name", condition = "#model.name != ''")
        public Model getForm(Model model) {
            ...
            return model;
        }
    
    
    注:key如果不填,默认是空,对应的值应该就是方法的参数的值了。由于参数可能不同,使key不同,而无法达到更新的目的。所有key需要设置。
    

    @CacheEvict

    value (也可使用 cacheNames):
        同Cacheable注解,可看做命名空间。表示删除哪个命名空间中的缓存
     
    allEntries:
        标记是否删除命名空间下所有缓存,默认为false 
    
    key:
        同Cacheable注解,代表需要删除的命名空间下唯一的缓存key。
    
    
    //访问这个方法之后删除对应的缓存,对应之前的Redis缓存注解的配置 。
    @CacheEvict(cacheNames = "product",key = "123",allEntries = true)
    public ModelAndView save(@Valid ProductForm productForm,BindingResult bindingResult,Map<String,Object> map){
        ...
    }
    

    @CachePut

    @CachePut(cacheNames = "product",key = "123")
    
        不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
        一般使用在保存,更新方法中。
    
    
    如果返回的是ModelAndView,由于ModelAndView无法序列化,返回ModelAndView不适合此注解。
    
    可以到Service层注解或者Dao层注解@CachePut
    

    Service使用@CachePut

    在整个类上注解:
        @CacheConfig(cacheNames = "product")    //配置整个类的缓存cacheNames
    
    
    Service方法注解:
    
        @CachePut(key = "123")
        public ProductInfo save(ProductInfo productInfo) {
            return productInfoDao.save(productInfo);
        }
    
  • 相关阅读:
    一个屌丝程序猿的人生(七十二)
    一个屌丝程序猿的人生(七十一)
    一个屌丝程序猿的人生(七十)
    一个屌丝程序猿的人生(六十九)
    一个屌丝程序猿的人生(六十八)
    一个屌丝程序猿的人生(六十七)
    Target-Action回调模式
    KVC & KVO
    ARC内存管理机制详解
    Objective-C中把URL请求的参数转换为字典
  • 原文地址:https://www.cnblogs.com/loveer/p/11316678.html
Copyright © 2011-2022 走看看