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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【Redis】SpringBoot集成Redis分布式锁以及Redis缓存

集成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>


第二个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

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


实现Redis分布式锁


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


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

简单的操作


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


set


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

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


delete


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

get


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

分布式锁


接下来就是讲分布式锁了。

假设在一个活动中,商品的特价出售,限时秒杀场景。比如双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);
        }
    }
}


这个是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));
    }
}

在上面是用Map来模拟查询数据库的操作了,sleep是为了模拟一些io操作的时间

你可以用apache ab工具进行高并发模拟。


Redis缓存


接下来就讲下缓存了

首先当然是导入Maven依赖咯

接下来就是在springboot启动类上加上注解:


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

因为我们上面已经在配置文件配置好了 redis的地址,账号。就不需要再配置了。

下面你就可以使用注解缓存了


在Controller层的使用


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


在这里,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){
 ...
}


但是假如我们不想使用CacheEvict删除缓存呢,只希望更新缓存呢,但是这里的返回值是ModelAndView,和前面的ResultVO不一样,而且无法序列化ModelAndView。所以在这里写注解,肯定只能是删除缓存的注解CacheEvict

其实我们可以去service层写缓存注解的,或者是Dao层,这样,返回对象是受我们控制的了。


在service层使用缓存


在整个类上注解


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

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


@Cacheable(key = "123") //注解缓存
public ProductInfo findOne(String productInfoId) {
    return productInfoDao.findOne(productInfoId);
}


@CachePut(key = "123") //和上面findOne的返回对象对应
public ProductInfo save(ProductInfo productInfo) {
    return productInfoDao.save(productInfo);
}

缓存注解的另外一些值


key我们是可以动态设置的


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

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


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

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


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


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


依据结果来判断是否缓存 unless = “#result.getCode() != 0”,#result其实就是ResultVO,也就是返回的对象

unless(除什么之外,如果不 的意思) 如果=0就缓存,需要写成!=0。理解起来就是,除了不等于0的情况之外,才缓存,也就是等于0才缓存。

其实就是,你想要什么条件下缓存,你写在这里面,把条件反过来写就行了


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


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


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
16天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
158 85
|
13天前
|
缓存 监控 NoSQL
Redis经典问题:缓存穿透
本文详细探讨了分布式系统和缓存应用中的经典问题——缓存穿透。缓存穿透是指用户请求的数据在缓存和数据库中都不存在,导致大量请求直接落到数据库上,可能引发数据库崩溃或性能下降。文章介绍了几种有效的解决方案,包括接口层增加校验、缓存空值、使用布隆过滤器、优化数据库查询以及加强监控报警机制。通过这些方法,可以有效缓解缓存穿透对系统的影响,提升系统的稳定性和性能。
|
2月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
2月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
1月前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
47 3
|
2月前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
89 6
|
2月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
46 5
|
2月前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
248 7
|
Java 应用服务中间件 Maven
传统maven项目和现在spring boot项目的区别
Spring Boot:传统 Web 项目与采用 Spring Boot 项目区别
515 0
传统maven项目和现在spring boot项目的区别
|
XML Java 数据库连接
创建springboot项目的基本流程——以宠物类别为例
创建springboot项目的基本流程——以宠物类别为例
157 0
创建springboot项目的基本流程——以宠物类别为例