Redis应用之分布式锁(set)
Redis应用之分布式锁(set)
在单机应用的场景下,我们常使用的锁主要是synchronized与Lock;但是在分布式横行的大环境下,显然仅仅这两种锁已经无法满足我们的需求;
需求:秒杀场景下,有若干服务实例,假设有2个,那么分别会有若干请求分别请求这2个服务实例。要求只能有一个请求秒杀成功,本质是秒杀方法在同一时间内只能被同一个线程执行,这就需要使用到分布式锁。
场景分布式锁
- 基于数据库实现
- 基于数据库实现分布式锁,主要使用InnoDB下的for update(如使用行级锁,需加唯一索引)
- 基于Zookeeper实现
- 在指定节点的目录下,创建一个唯一的瞬时有序节点。可以使用Curator去实现。
- 基于缓存实现(redis)
- 主要使用set(setnx用法有缺陷且过时)
详解redis的set命令
我们已知道set用于设置String类型的key/value值,如下:
127.0.0.1:6379> set name gaoyuan
OK
127.0.0.1:6379> get name
"gaoyuan"
setnx + expire = 非原子性
在redis2.6.12版本之前,分布式锁常使用setnx来实现。setnx是set if not exists
的意思,也就是当值不存在时,才可以创建成功,这样就能保证在同一时间只能有个设置成功。
但是,setnx无法在插入值的同时设置超时时间,setnx 与 expire 是两条独立的语句,这样加锁操作就是非原子性的,那么就会带来问题。(比如,当setnx成功后,准备执行expire前,程序突然出现错误,则添加的数据就无法清除了,因为没有超时时间,不会自动清除)
set key value [EX seconds] [PX milliseconds] [NX|XX]
在redis2.6.12版本之后,redis支持通过set在设置值得同时设置超时时间,此操作是原子操作。
// 设置lock的值为123,存在6秒
127.0.0.1:6379> set lock 123 EX 6 NX
OK
// 6秒内,重复设置lock的值为123,返回nil(也就是null)
127.0.0.1:6379> set lock 123 EX 6 NX
(nil)
// 6秒内,获取值,能够获取到
127.0.0.1:6379> get lock
"123"
// 6秒后,获取值,获取为nil,又可以重新set值了
127.0.0.1:6379> get lock
(nil)
下面我们利用set的特性来实现分布式锁。
实现分布式锁
我们先看一个不加锁的例子
我们先构造一个对象 MyThread
class MyThread implements Runnable{
int i = 0;
@Override
public void run() {
try {
for(int j=0;j<10;j++){
i = i + 1;
// 这里延时,为了让其他线程进行干扰
TimeUnit.MILLISECONDS.sleep(10);
i = i - 1;
System.out.println("i=" + i);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
执行
ExecutorService executorService = Executors.newFixedThreadPool(3);
MyThread myThread = new MyThread();
executorService.submit(myThread);
executorService.submit(myThread);
executorService.submit(myThread);
executorService.shutdown();
输出
i=0
i=0
i=0
i=3
i=3
i=3
i=4
i=4
...
可以看出,i居然会出现不等于0的情况。
Redis加锁(set命令)
获取锁的方法
/**
* 获取锁
* 利用set key value [EX seconds] [PX milliseconds] [NX|XX] 命令实现锁机制
* @author GaoYuan
*/
public static String tryLock(Jedis jedis, int timeout) throws Exception{
if(timeout == 0){
timeout = 5000;
}
String returnId = null;
// 生成随机标识
String id = UUID.randomUUID().toString();
// 设置锁超时10秒
int lockExpireMs = 10000;
long startTime = System.currentTimeMillis();
// 超时时间内循环获取
while ((System.currentTimeMillis() - startTime) < timeout){
String result = jedis.set(lockKey, id, "NX", "PX", lockExpireMs);
if(result != null){
returnId = id;
break;
}
TimeUnit.MILLISECONDS.sleep(100);
}
if(returnId == null){
// 获取锁超时,抛出异常
throw new Exception("获取锁超时");
}
// 将set的值返回,用于后续的解锁
return returnId;
}
释放锁的方法(释放锁的方式有两种)
释放方法一:
/**
* 释放锁 - 利用redis的watch + del
* @author GaoYuan
*/
public static boolean unLock(Jedis jedis, String id){
boolean result = false;
while(true){
if(jedis.get(lockKey) == null){
return false;
}
// 配置监听
jedis.watch(lockKey);
// 这里确保是加锁者进行解锁
if(id!=null && id.equals(jedis.get(lockKey))){
Transaction transaction = jedis.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if(results == null){
continue;
}
result = true;
}
// 释放监听
jedis.unwatch();
break;
}
return result;
}
释放方法二:
/**
* 释放锁 - 利用lua脚本
* @author GaoYuan
*/
public static boolean unLockByLua(Jedis jedis, String id){
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(id));
if (Objects.equals(1, result)) {
return true;
}
return false;
}
改造之前的例子
class MyThread implements Runnable{
int i = 0;
@Override
public void run() {
try {
for(int j=0;j<10;j++){
Jedis jedis = new Jedis(JedisConfig.HOST, JedisConfig.PORT);
try {
// 尝试获取锁,有超时时间
String id = RedisLock.tryLock(jedis,5000);
i = i + 1;
// 这里延时,为了让其他线程进行干扰(当然,加锁就不会有干扰)
TimeUnit.MILLISECONDS.sleep(10);
i = i - 1;
// 加锁后,期望值 i=0
System.out.println("i=" + i);
// 释放锁
RedisLock.unLock(jedis, id);
}catch (Exception e){
// e.printStackTrace();
System.out.println("获取锁超时");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
运行输出
i=0
i=0
i=0
i=0
i=0
i=0
...
将run方法中的延时时间设置成1秒(1000)后,会打印超时的情况
i=0
i=0
i=0
获取锁超时
获取锁超时
i=0
...
至此利用jedis实现了分布式锁。
码云
完整代码见: https://gitee.com/gmarshal/foruo-demo/tree/master/foruo-demo-redis/foruo-demo-redis-lock
博客
开源中国博客地址
https://my.oschina.net/gmarshal/blog/2120428
个人博客地址
欢迎关注我的个人微信订阅号:(据说这个头像程序猿专用)

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Android开发应用程序生成以太坊钱包
Android应用程序以太坊钱包生成,要做的工作不少,不过如果我们一步一步来应该也比较清楚: 1.在app/build.gradle中集成以下依赖项: compile ('org.web3j:core-android:2.2.1') web3j核心是用于从服务器下载以太坊区块链数据的核心类库。它通常用于以太坊开发。 2.我们将设计一个Android UI示例,屏幕上将有文本编辑和按钮。在EditText中,将要求用户输入钱包的密码。然后在按钮的单击事件上,我们将开始发送密码的过程。以下是layout.xml文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/content" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation=...
-
下一篇
用高德sdk做一个滴滴司机端的导航地图(自定义地图导航界面)
用高德sdk做一个滴滴司机端的导航。 主要的导航功能是在NaviFragment中。 效果如下: 下载apk:下载地址 扫一扫下载apk 第一步:集成高德sdk 请看这篇文章 集成Android高德SDK 第二步:四个重要的类 1.AMapNaviView 导航地图控件,导航路线都是在这个上面绘制的 //AMapNaviView一些重要方法 //获取绘制路线所需的Amap类。 AMapNaviView.getMap()。 //AMapNaviView有生命周期方法 需要我们和Activity或者Fragment的生命周期保持一致。 //在Activity的onCreate调用 在Fragment的onViewCreate调用 AMapNaviView.onCreate(savedInstanceState)。 //在Activity或者Fragment的onResume中调用 AMapNaviView.onResume(); //在Activity或者Fragment的onPause中调用 AMapNaviView.onPause(); //在Activity或者Fragment的on...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- 面试大杂烩
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程