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/
本文源自【谙忆的博客】
SpringBoot集成Redis分布式锁以及Redis缓存的更多相关文章
- [转帖]SpringBoot集成redisson分布式锁
SpringBoot集成redisson分布式锁 https://www.cnblogs.com/yangzhilong/p/7605807.html 前几天同事刚让增加上这一块东西. 百度查一下 啥 ...
- SpringBoot集成redisson分布式锁
官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 20180226更新:增加tryLock方法,建议后面去掉Distr ...
- springboot(十二)-分布式锁(redis)
什么是分布式锁? 要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁.进程锁. 线程锁:主要用来给方法.代码块加锁.当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段.线程锁只在同一 ...
- Redis分布式锁(ServiceStack.Redis实现)
1.设计思路 由于Redis是单线程模型,命令操作原子性,所以利用这个特性可以很容易的实现分布式锁.A用户端在Resdis写入1个KEY,其他的用户无法写入这个KEY,实现锁的效果.A用户使用完成后释 ...
- redis分布式锁在springboot中的实现
理论知识 redis分布式锁的实现方案请参考文章 如何优雅地用redis实现分布式锁 本案例简介 以秒杀活动为例子,在多线程高并发的情况下需要保证秒杀业务的线程安全性,确保秒杀记录与所扣库存数 ...
- springBoot实现redis分布式锁
参考:https://blog.csdn.net/weixin_44634197/article/details/108308395 .. 使用redis的set命令带NX(not exist)参数实 ...
- Redis专题(3):锁的基本概念到Redis分布式锁实现
拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务.分布式框架.ZooKeeper.SpringCloud等等 ...
- 【分布式锁】Redis实现可重入的分布式锁
一.前言 之前写的一篇文章<细说分布式锁>介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现.自定义分布式锁注解以及需要注意的问题都没描述.本文就是详细说明如何利用 ...
- spring-boot 中实现标准 redis 分布式锁
一,前言 redis 现在已经成为系统缓存的必备组件,针对缓存读取更新操作,通常我们希望当缓存过期之后能够只有一个请求去更新缓存,其它请求依然使用旧的数据.这就需要用到锁,因为应用服务多数以集群方式部 ...
随机推荐
- 性能测试二十二:环境部署之Nginx
由于单纯用tomcat只能通过ip+端口号的形式访问,这样只能访问一个tomcat,而真实项目中又不可能只用一两个tomcat,所以就需要Nginx来进行分配访问请求, Nginx本身性能非常好,据官 ...
- git merge简介
git merge的基本用法为把一个分支或或某个commit的修改合并到现在的分支上.我们可以运行git merge -h和git merge --help查看其命令,后者会直接转到一个网页(git的 ...
- POJ 2376 Cleaning Shifts【贪心】
POJ 2376 题意: 给出一给大区间和n各小区间,问最少可以用多少小区间覆盖整个大区间. 分析: 贪心法.设t为当前所有已确定区间的最右端,那我们可以每次都取所有可选的小区间(左端点<=t+ ...
- Docker 注意事项
一.Dockerfile名字不能是大写. 二.Docker-compares 引用自:https://www.cnblogs.com/wj5633/p/6707012.html 引用自:https:/ ...
- 初探和实现websocket心跳重连(npm: websocket-heartbeat-js)
提示:文章最下方有仓库地址 心跳重连缘由 websocket是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒.因此为了保证连接的可持续性和稳定性,websocket ...
- Consul服务器配置
微服务带来最大的好处就是把整个大项目分割成不同的服务,运行在不同服务器上,实现解耦和分布式处理.微服务虽然有很多好处,但是也会有不好的一方面.任何事物都会有两面性,在微服务里面运维会是一个很大的难题, ...
- IdentityServer4-快速入门
一.设置和概述 二.使用客户端凭证保护API 三.使用密码保护API 四.使用OpenID Connect添加用户验证 五.添加对外部认证的支持 六.切换到Hybrid Flow并添加API访问权限 ...
- android studio 汉化
the modules below are not imported from Gradle anymore. Check those to be removed from the ide proje ...
- 安卓工作室 android studio 谷歌账号 登录
作者:韩梦飞沙 Author:han_meng_fei_sha 邮箱:313134555@qq.com E-mail: 313134555 @qq.com 登录你的谷歌账号,开始 添加 云功能 到你的 ...
- [HackerRank]Choosing White Balls
[HackerRank]Choosing White Balls 题目大意: 有\(n(n\le30)\)个球排成一行,每个球的颜色为黑或白. 执行\(k\)次操作,第\(i\)次操作形式如下: 从\ ...