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);
    }
原文地址:https://www.cnblogs.com/loveer/p/11316678.html