SpringBoot进阶教程(二十七)整合Redis之分布式锁
在之前的一篇文章(《Java分布式锁,搞懂分布式锁实现看这篇文章就对了》),已经介绍过几种java分布式锁,今天来个Redis分布式锁的demo。redis 现在已经成为系统缓存的必备组件,针对缓存读取更新操作,通常我们希望当缓存过期之后能够只有一个请求去更新缓存,其它请求依然使用旧的数据。这就需要用到锁,因为应用服务多数以集群方式部署,因此这里的锁就必需要是分布式锁才能符合需求。
学习本章节之前,建议依次阅读以下文章,更好的串联全文内容,如已掌握以下列出知识点,请跳过:
v简单实现
锁是针对某个资源的状态,保证其访问的互斥性,在实际使用当中,这个状态一般是一个字符串。使用 Redis 实现锁,主要是将状态放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个状态,就不允许之后的一些操作。spring boot使用Redis的操作主要是通过RedisTemplate(或StringRedisTemplate )来实现。
1.1 将锁状态放入 Redis:
redisTemplate.opsForValue().setIfAbsent("lockkey", "value"); // setIfAbsent如果键不存在则新增,存在则不改变已经有的值。
1.2 设置锁的过期时间
redisTemplate.expire("lockkey", 30000, TimeUnit.MILLISECONDS);
1.3 删除/解锁
redisTemplate.delete("lockkey");
这么就是简单实现,但是1.1和1.2这么做,这两步违背了原子性,也就是一旦锁被创建,而没有设置过期时间,则锁会一直存在。
1.4 获取锁
redisTemplate.opsForValue().get("lockkey");
1.5 解决方案
spring data的 RedisTemplate 当中并没有这样的方法。但是在jedis当中是有这种原子操作的方法的,需要通过 RedisTemplate 的 execute 方法获取到jedis里操作命令的对象.
String result = template.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, "锁定的资源", "NX", "PX", 3000);
}
});
注意: Redis 从2.6.12版本开始 set 命令支持 NX 、 PX 这些参数来达到 setnx 、 setex 、 psetex 命令的效果,文档参见: SET — Redis 命令参考
NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。
v锁的进阶
模拟一个比较常见的秒杀场景,这时候就需要用到锁。
2.1 创建RedisLockHelper
package com.demo.common; import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component; /**
* Created by toutou on 2019/1/27.
*/
@Component
@Slf4j
public class RedisLockHelper {
@Autowired
private StringRedisTemplate stringRedisTemplate; /**
* 加锁
* @param targetId targetId - 商品的唯一标志
* @param timeStamp 当前时间+超时时间 也就是时间戳
* @return
*/
public boolean lock(String targetId,String timeStamp){
if(stringRedisTemplate.opsForValue().setIfAbsent(targetId,timeStamp)){
// 对应setnx命令,可以成功设置,也就是key不存在
return true;
} // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentLock = stringRedisTemplate.opsForValue().get(targetId);
// 如果锁过期 currentLock不为空且小于当前时间
if(!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
// 获取上一个锁的时间value 对应getset,如果lock存在
String preLock =stringRedisTemplate.opsForValue().getAndSet(targetId,timeStamp); // 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
// 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock) ){
// preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
return false;
} /**
* 解锁
* @param target
* @param timeStamp
*/
public void unlock(String target,String timeStamp){
try {
String currentValue = stringRedisTemplate.opsForValue().get(target);
if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp) ){
// 删除锁状态
stringRedisTemplate.opsForValue().getOperations().delete(target);
}
} catch (Exception e) {
log.error("警报!警报!警报!解锁异常{}",e);
}
}
}
这个是Redis加锁和解锁的工具类,里面使用的主要是两个命令,SETNX和GETSET。
SETNX命令 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
GETSET命令 先查询出原来的值,值不存在就返回nil。然后再设置值 对应的Java方法在代码中提示了。 注意一点的是,Redis是单线程的!所以在执行GETSET和SETNX不会存在并发的情况。
2.2 创建Controller模拟秒杀场景
package com.demo.controller; import com.demo.common.RedisLockHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; /**
* Created by toutou on 2019/1/27.
*/
@RestController
@Slf4j
public class RedisController { @Autowired
RedisLockHelper redisLockHelper; /**
* 超时时间 5s
*/
private static final int TIMEOUT = 5*1000; @RequestMapping(value = "/seckilling")
public String Seckilling(String targetId){
//加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLockHelper.lock(targetId,String.valueOf(time))){
return "排队人数太多,请稍后再试.";
} int surplusCount = 0;
// 查询该商品库存,为0则活动结束 e.g. getStockByTargetId
if(surplusCount==0){
return "活动结束.";
}else {
// 下单 e.g. buyStockByTargetId //减库存 不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
surplusCount =surplusCount-1;
try{
Thread.sleep(100);//模拟减库存的处理时间
}catch (InterruptedException e){
e.printStackTrace();
}
// 减库存操作数据库 e.g. updateStockByTargetId // buyStockByTargetId 和 updateStockByTargetId 可以同步完成(或者事物),保证原子性。
} //解锁
redisLockHelper.unlock(targetId,String.valueOf(time)); return "恭喜您,秒杀成功。";
}
}
其他参考资料:
- spring boot redis分布式锁
- spring-boot 中实现标准 redis 分布式锁
- Spring boot redis分布式锁
- SpringBoot集成Redis分布式锁以及Redis缓存
注:本文中很多内容来自以上链接的学习心得,感谢以上人员分享,也请转载本文的各站保持以上链接。
v源码地址
https://github.com/toutouge/javademosecond/tree/master/hellospringboot
作 者:请叫我头头哥
出 处:http://www.cnblogs.com/toutou/
关于作者:专注于基础平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!
SpringBoot进阶教程(二十七)整合Redis之分布式锁的更多相关文章
- SpringBoot进阶教程(三十)整合Redis之Sentinel哨兵模式
Redis-Sentinel是官方推荐的高可用解决方案,当redis在做master-slave的高可用方案时,假如master宕机了,redis本身(以及其很多客户端)都没有实现自动进行主备切换,而 ...
- SpringBoot进阶教程(二十九)整合Redis 发布订阅
SUBSCRIBE, UNSUBSCRIBE 和 PUBLISH 实现了 发布/订阅消息范例,发送者 (publishers) 不用编程就可以向特定的接受者发送消息 (subscribers). Ra ...
- SpringBoot进阶教程(二十八)整合Redis事物
Redis默认情况下,事务支持被禁用,必须通过设置setEnableTransactionSupport(true)为使用中的每个redistplate显式启用.这样做会强制将当前重新连接绑定到触发m ...
- SpringBoot进阶教程(二十六)整合Redis之共享Session
集群现在越来越常见,当我们项目搭建了集群,就会产生session共享问题.因为session是保存在服务器上面的.那么解决这一问题,大致有三个方案,1.通过nginx的负载均衡其中一种ip绑定来实现( ...
- SpringBoot进阶教程(二十五)整合Redis之@Cacheable、@CachePut、@CacheEvict的应用
在上一篇文章(<SpringBoot(二十四)整合Redis>)中,已经实现了Spring Boot对Redis的整合,既然已经讲到Cache了,今天就介绍介绍缓存注解.各家互联网产品现在 ...
- SpringBoot进阶教程(二十四)整合Redis
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力.Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非 ...
- SpringBoot进阶教程(七十三)整合elasticsearch
Elasticsearch 是一个分布式.高扩展.高实时的搜索与数据分析引擎.它能很方便的使大量数据具有搜索.分析和探索的能力.充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更 ...
- SpringBoot的学习二:整合Redis,JPA,Mybatis
Redis介绍: 是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API 特性: Redis 与其他 key - value 缓 ...
- SpringBoot进阶教程(二十三)Linux部署Quartz
在之前的一篇文章中<SpringBoot(九)定时任务Schedule>,已经详细介绍了关于schedule框架的配置和使用,有收到一些朋友关于部署的私信,所以抽时间整理一个linux部署 ...
随机推荐
- oracle中通过sql查询sde中图形面积
select st_area(shape) from XAG2011430200000M_DLTB t where objectid=330
- printf输出格式介绍(转)
格式代码 A ABC ABCDEFGH %S A ABC ABCDEFGH %5S ####A ##ABC ABCDEFGH %.5S A ABC ABCDE %5.5S ####A ##ABC AB ...
- 解密TTY
本文内容来自The TTY demystified ,讲述了*NIX系统中TTY的历史与工作原理,看完后解决了我很多疑惑,于是做此翻译,与大家分享. 译者:李秋豪 江家伟 审校: V1.0 Sun M ...
- Vue.js——60分钟组件快速入门
一.组件简介 组件系统是Vue.js其中一个重要的概念,它提供了一种抽象,让我们可以使用独立可复用的小组件来构建大型应用,任意类型的应用界面都可以抽象为一个组件树: 那么什么是组件呢?组件可以扩展HT ...
- 一分钟告诉你究竟DevOps是什么鬼?
历史回顾 为了能够更好的理解什么是DevOps,我们很有必要对当时还只有程序员(此前还没有派生出开发者,前台工程师,后台工程师之类)这个称号存在的历史进行一下回顾. 如编程之道中所言: 老一辈的程序员 ...
- spring的7个模块
Spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的.框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架. Spring ...
- 影响 MySQL Server 性能的相关因素
MySQL 最多的使用场景是WEB 应用,那么我们就以一个WEB 应用系统为例,逐个分析其系统构成,进行经验总结,分析出数据库应用系统中各个环境对性能的影响. 商业需求对性能的影响 这里我们就拿一个看 ...
- 设置ActiveMQ的访问密码
1.设置ActiveMQ的访问密码,以提高ActiveMQ的安全性 2.在ActiveMQ的conf目录的activemq.xml中添加账号密码 2.1 添加的代码如下 <!-- 添加访问Ac ...
- 十九、Hadoop学记笔记————Hbase和MapReduce
概要: hadoop和hbase导入环境变量: 要运行Hbase中自带的MapReduce程序,需要运行如下指令,可在官网中找到: 如果遇到如下问题,则说明Hadoop的MapReduce没有权限访问 ...
- 附录C--拉格朗日对偶性
1.原始问题 假设$f(x)$,$c_i(x)$,$h_j(x)$是定义在$R^n$上的连续可微函数,$x \in R^n$.考虑以下三类优化问题. 1.无约束的优化问题: \begin{align* ...