SpringBoot集成Redis分布式锁以及Redis缓存

https://blog.csdn.net/qq_26525215/article/details/79182687

集成Redis

首先在pom.xml中加入需要的redis依赖和缓存依赖

<!-- 引入redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-start-cache</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第二个spring-boot-start-cache的依赖,是使用缓存注解需要的,我在项目中没有引入。 
因为我在websocket中已经引入了。 
查询依赖关系 ctrl+shift+alt+u 快捷键(也可以在pom.xml文件上右键->Maven->Show Dependencies…)查询maven包依赖引入关系,ctrl+f搜索包

SpringBoot的yml配置文件下增加redis的配置:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: chenhaoxiang
  • 1
  • 2
  • 3
  • 4
  • 5

输入你自己Redis服务器的地址,端口和密码,没有密码的就不要password了。

实现Redis分布式锁

在类中直接使用如下代码即可注入Redis的操作类

@Autowired
private StringRedisTemplate stringRedisTemplate;//可以写很多类型的值
  • 1
  • 2

简单的操作

更多的Redis内容请看: http://redis.cn/

set

//设置key-value和过期时间
stringRedisTemplate.opsForValue().set("key","value",7200, TimeUnit.SECONDS);//key,value,过期时间,时间单位 s
  • 1
  • 2

使用存储的时候,最后要设置一个过期时间,就算是几年,你也要设置一个过期时间。否则会一直占用存储空间的

delete

stringRedisTemplate.opsForValue().getOperations().delete("key");//删除key对应的键值对
  • 1

get

stringRedisTemplate.opsForValue().get("key");//获取对应key的value
  • 1

分布式锁

接下来就是讲分布式锁了。 
假设在一个活动中,商品的特价出售,限时秒杀场景。比如双11的。 
通常的做法,有乐观锁和悲观锁 
介绍乐观锁和悲观锁是什么我就不介绍了。 
其实这里的Redis分布式锁也算是一种乐观锁。也就是即使资源被锁了,后来的用户不会被阻塞,而是返回异常/信息给你,告诉你操作(在这里是抢购)不成功。

实现起来很简单。看下面的类:

package cn.chenhaoxiang.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * Created with IntelliJ IDEA.
 * User: 陈浩翔.
 * Date: 2018/1/26.
 * Time: 下午 10:05.
 * Explain: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)){//对应setnx命令
            //可以成功设置,也就是key不存在
            return true;
        }

        //判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        //如果锁过期
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){//currentValue不为空且小于当前时间
            //获取上一个锁的时间value
            String oldValue =stringRedisTemplate.opsForValue().getAndSet(key,value);//对应getset,如果key存在

            //假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentValue=A(get取的旧的值肯定是一样的),两个线程的value都是B,key都是K.锁时间已经过期了。
            //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的value已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
                //oldValue不为空且oldValue等于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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

这个是Redis加锁和解锁的工具类 
里面使用的主要是两个命令,SETNX和GETSET。 
SETNX命令 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做 
GETSET命令 先查询出原来的值,值不存在就返回nil。然后再设置值 
对应的Java方法在代码中提示了。 
注意一点的是,Redis是单线程的!所以在执行GETSET和SETNX不会存在并发的情况。

下面来看我们使用该类加锁解锁的类:

package cn.chenhaoxiang.service.impl;

import cn.chenhaoxiang.exception.SellException;
import cn.chenhaoxiang.service.RedisLock;
import cn.chenhaoxiang.service.SeckillService;
import cn.chenhaoxiang.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: 陈浩翔.
 * Date: 2018/1/26.
 * Time: 下午 9:30.
 * Explain:
 */
@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);
    }

    //解决方法二,基于Redis的分布式锁 http://redis.cn/commands/setnx.html  http://redis.cn/commands/getset.html
    //SETNX命令  将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
    // GETSET命令  先查询出原来的值,值不存在就返回nil。然后再设置值
    //支持分布式,可以更细粒度的控制
    //多台机器上多个线程对一个数据进行操作的互斥。
    //Redis是单线程的!!!
    @Override
    public void orderProductMocckDiffUser(String productId) {//解决方法一:synchronized锁方法是可以解决的,但是请求会变慢,请求变慢是正常的。主要是没做到细粒度控制。比如有很多商品的秒杀,但是这个把所有商品的秒杀都锁住了。而且这个只适合单机的情况,不适合集群

        //加锁
        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;//不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
            try{
                Thread.sleep(100);//模拟减库存的处理时间
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            stock.put(productId,stockNum);
        }

        //解锁
        redisLock.unlock(productId,String.valueOf(time));

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94

在上面是用Map来模拟查询数据库的操作了,sleep是为了模拟一些io操作的时间 
你可以用apache ab工具进行高并发模拟。

Redis缓存

接下来就讲下缓存了 
首先当然是导入Maven依赖咯 
接下来就是在springboot启动类上加上注解:

@EnableCaching //缓存支持  配置Redis缓存需要的
  • 1

因为我们上面已经在配置文件配置好了 redis的地址,账号。就不需要再配置了。 
下面你就可以使用注解缓存了

在Controller层的使用

//Redis缓存注解  Cacheable第一次访问会访问到方内的内容,方法会返回一个对象,返回对象的时候,会把这个对象存储。下一次访问的时候,不会进去这个方法,直接从redis缓存中拿
@Cacheable(cacheNames = "product",key = "123")
public ResultVO list(){
...
}
  • 1
  • 2
  • 3
  • 4
  • 5

在这里,product其实就相当于一个命名空间。key的话,在更新缓存,删除缓存的时候用到的。 
注意,方法返回的对象加了缓存注解的,一定要实现序列化!

然后,我们可以在增删改的地方加上删除缓存,或者更新缓存的注解。

@CacheEvict(cacheNames = "product",key = "123") //访问这个方法之后删除对应的缓存  对应之前的Redis缓存注解的配置 。key如果不填,默认是空,对应的值应该就是方法的参数的值了.对应BuyerProductController-list方法的缓存
//    @CachePut(cacheNames = "product",key = "123") //对应之前的Redis缓存注解的配置
    //@CachePut 每次还是会执行方法中的内容,每次执行完成后会把返回的内容放到Redis中去.
    // 这种注解和原来对应的返回对象需要是相同的才行,这里返回的是ModelAndView。可以到service层注解或者dao层注解CachePut
    public ModelAndView save(@Valid ProductForm productForm,
                             BindingResult bindingResult,
                             Map<String,Object> map){
 ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但是假如我们不想使用CacheEvict删除缓存呢,只希望更新缓存呢,但是这里的返回值是ModelAndView,和前面的ResultVO不一样,而且无法序列化ModelAndView。所以在这里写注解,肯定只能是删除缓存的注解CacheEvict 
其实我们可以去service层写缓存注解的,或者是Dao层,这样,返回对象是受我们控制的了。

在service层使用缓存

在整个类上注解

@CacheConfig(cacheNames = "product") //配置整个类的缓存cacheNames,相当于作用域
  • 1

这样,这个类下的方法就不用再写cacheNames了 。

@Cacheable(key = "123") //注解缓存
public ProductInfo findOne(String productInfoId) {
    return productInfoDao.findOne(productInfoId);
}
  • 1
  • 2
  • 3
  • 4
@CachePut(key = "123") //和上面findOne的返回对象对应
public ProductInfo save(ProductInfo productInfo) {
    return productInfoDao.save(productInfo);
}
  • 1
  • 2
  • 3
  • 4

缓存注解的另外一些值

key我们是可以动态设置的

@Cacheable(cacheNames = "product",key = "#sellerId")//sellerId为方法中的参数名,这样,key就是动态配置了
public ResultVO list(String sellerId){
...
}
  • 1
  • 2
  • 3
  • 4

可以根据参数来进行判断,是否缓存

@Cacheable(cacheNames = "product",key = "#sellerId",condition = "#sellerId.length() > 3")
public ResultVO list(String sellerId){
...
}
  • 1
  • 2
  • 3
  • 4

这样只有条件成立才会直接返回缓存,结果不成立是不缓存的,即使有缓存,也会运行方法

还可以根据返回结果来判断是不是缓存这个结果

@Cacheable(cacheNames = "product",key = "#sellerId",unless = "#result.getCode() != 0")
public ResultVO list(String sellerId){
...
}
  • 1
  • 2
  • 3
  • 4

依据结果来判断是否缓存 unless = “#result.getCode() != 0”,#result其实就是ResultVO,也就是返回的对象 
unless(除什么之外,如果不 的意思) 如果=0就缓存,需要写成!=0。理解起来就是,除了不等于0的情况之外,才缓存,也就是等于0才缓存。 
其实就是,你想要什么条件下缓存,你写在这里面,把条件反过来写就行了

你如果测试缓存的话,你可以在方法内打一个断点进行测试。没有运行那个方法就获取到数据了,证明缓存生效了。

最后,注意,返回的缓存对象一定要实现序列化!!!

项目地址:

GITHUB源码下载地址:【点我进行访问

本文章由[谙忆]编写, 所有权利保留。 
欢迎转载,分享是进步的源泉。

转载请注明出处:http://chenhaoxiang.cn/2018/01/27/0104/
本文源自【谙忆的博客

原文地址:https://www.cnblogs.com/ruiati/p/9622775.html

时间: 2024-10-29 10:46:50

SpringBoot集成Redis分布式锁以及Redis缓存的相关文章

SpringBoot集成redisson分布式锁

官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 1.引用redisson的pom <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.5.0</version> </dependency> 2.定义Lo

[转帖]SpringBoot集成redisson分布式锁

https://www.cnblogs.com/yangzhilong/p/7605807.html 前几天同事刚让增加上这一块东西. 百度查一下 啥意思.. 学习一下. 官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 20180226更新:增加tryLock方法,建议后面去掉DistributedLocker接口和其实现类,直接在RedissLockUtil中注入RedissonClient实现类(简单但会丢失

redis 分布式锁 获取redis的key

今天翻看去年写的代码,发现了有一块用redis写了分布式锁,业务场景就是,再搭建第一版爬虫的时候,用来 定时任务来获取redis里的key值,保证同一时间只有一台机器拿到这个key,用到这样的办法来做,可以后来发现,机器多的话,反而花费时间在争抢锁上,毫无意义,也就放弃了这版爬虫框架. 下面是代码,自己可以拿去试试,调调,因为用的时间短,可能会有bug. /** * 获取redis对应的爬虫key的锁 * @param lockKey * @return */ public boolean ob

Redis专题(3):锁的基本概念到Redis分布式锁实现

拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务.分布式框架.ZooKeeper.SpringCloud等等.本文先回顾锁的概念,再介绍分布式锁,以及如何用Redis来实现分布式锁. 一.锁的基本了解 首先,回顾一下我们工作学习中的锁的概念. 为什么要先讲锁再讲分布式锁呢? 我们都清楚,锁的作用是要解决多线程对共享资源的访问而产生的线程安全问题,而在平时生活中用到锁的情况其实并不多,可能有些朋友对锁的概念

【分布式锁】Redis实现可重入的分布式锁

一.前言 之前写的一篇文章<细说分布式锁>介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现.自定义分布式锁注解以及需要注意的问题都没描述.本文就是详细说明如何利用Redis实现重入的分布式锁. 二.方案 死锁问题 当一个客户端获取锁成功之后,假如它崩溃了导致它再也无法和 Redis 节点通信,那么它就会一直持有这个锁,导致其它客户端永远无法获得锁了,因此锁必须要有一个自动释放的时间.   我们需要保证setnx命令和expire命令以原子的方式执行,否则如果客户端执行

【分布式缓存系列】集群环境下Redis分布式锁的正确姿势

一.前言 在上一篇文章中,已经介绍了基于Redis实现分布式锁的正确姿势,但是上篇文章存在一定的缺陷——它加锁只作用在一个Redis节点上,如果通过sentinel保证高可用,如果master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况: 客户端1在Redis的master节点上拿到了锁 Master宕机了,存储锁的key还没有来得及同步到Slave上 master故障,发生故障转移,slave节点升级为master节点 客户端2从新的Master获取到了对应同一个资源的锁 于是,客

死磕 java同步系列之redis分布式锁进化史

问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API. 本章我们将介绍如何基于redis实现分布式锁,并把其实现的进化史从头到尾讲明白,以便大家在面试的时候能讲清楚

redis分布式锁和消息队列

最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP等,不能在内存中使用锁,或者如Java这样的,需要一下更为简单的锁校验的时候,redis分布式锁的使用就足够满足了.redis的分布式锁其实就是基于setnx方法和redis对key可设置有效时间的功能来实现的.基本用法比较简单. public boolean tryLock(String lock

Redis分布式锁及分区

以下内容是翻译的官网文档RedLock和分区部分,可以简单了解分布式锁在redis如何实现及其方式 redis分区的方法 redis实现的分布式锁RedLock算法,分布式锁,即在多个master上获取同一个锁 1.in order to get the lock,the client get the current ms time 2.顺序对n个实例获取锁权限(n个都是master),尝试锁时,设置连接超时时间,防止由于实例挂了,导致长时间无法执行操作 3.计算为了获取锁消耗的时间,有且仅有,