分布式锁的实现

锁是用来解决什么问题的;

  1. 一个进程中的多个线程,多个线程并发访问同一个资源的时候,如何解决线程安全问题。
  2. 一个分布式架构系统中的两个模块同时去访问一个文件对文件进行读写操作
  3. 多个应用对同一条数据做修改的时候,如何保证数据的安全性

在单进程中,我们可以用到synchronized、lock之类的同步操作去解决,但是对于分布式架构下多进程的情况下,如何做到跨进程的锁。就需要借助一些第三方手段来完成

设计一个分布式所需要解决的问题

分布式锁的解决方案

  1. 怎么去获取锁

数据库,通过唯一约束

lock(

id  int(11)

methodName  varchar(100),

memo varchar(1000)

modifyTime timestamp

unique key mn (method)  --唯一约束

)

获取锁的伪代码

try{

exec  insert into lock(methodName,memo) values(‘method’,’desc’);    method

return true;

}Catch(DuplicateException e){

return false;

}

释放锁

delete from lock where methodName=’’;

存在的需要思考的问题

  1. 锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁
  2. 锁是非阻塞的,数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
  3. 锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁

zookeeper实现分布式锁

利用zookeeper的唯一节点特性或者有序临时节点特性获得最小节点作为锁. zookeeper 的实现相对简单,通过curator客户端,已经对锁的操作进行了封装,原理如下

zookeeper的优势

1.  可靠性高、实现简单

2.  zookeeper因为临时节点的特性,如果因为其他客户端因为异常和zookeeper连接中断了,那么节点会被删除,意味着锁会被自动释放

3.  zookeeper本身提供了一套很好的集群方案,比较稳定

4.  释放锁操作,会有watch通知机制,也就是服务器端会主动发送消息给客户端这个锁已经被释放了

基于缓存的分布式锁实现

redis中有一个setNx命令,这个命令只有在key不存在的情况下为key设置值。所以可以利用这个特性来实现分布式锁的操作

具体实现代码

  1. 添加依赖包
  1. 编写redis连接的代码

释放锁的代码

  1. 分布式锁的具体实现
  1. 怎么释放锁

redis多路复用机制

linux的内核会把所有外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个 file descriptor(文件描述符)。对于一个socket的读写也会有响应的描述符,称为socketfd(socket 描述符)。而IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程

IO多路复用又称为事件驱动,操作系统提供了一个功能,当某个socket可读或者可写的时候,它会给一个通知。当配合非阻塞socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据。操作系统的功能通过select/pool/epoll/kqueue之类的系统调用函数来使用,这些函数可以同时监视多个描述符的读写就绪情况,这样多个描述符的I/O操作都能在一个线程内并发交替完成,这就叫I/O多路复用,这里的复用指的是同一个线程

多路复用的优势在于用户可以在一个线程内同时处理多个socket的 io请求。达到同一个线程同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到目的

redis中使用lua脚本

lua脚本

Lua是一个高效的轻量级脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能

使用脚本的好处

  1. 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
  2. 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
  3. 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑

Lua在linux中的安装

到官网下载lua的tar.gz的源码包

tar -zxvf lua-5.3.0.tar.gz

进入解压的目录:

cd lua-5.2.0

make linux  (linux环境下编译)

make install

如果报错,说找不到readline/readline.h, 可以通过yum命令安装

yum -y install readline-devel ncurses-devel

安装完以后再make linux  / make install

最后,直接输入 lua命令即可进入lua的控制台

lua的语法

Redis与Lua

在Lua脚本中调用Redis命令,可以使用redis.call函数调用。比如我们调用string类型的命令

redis.call(‘set’,’hello’,’world’)

redis.call 函数的返回值就是redis命令的执行结果。前面我们介绍过redis的5中类型的数据返回的值的类型也都不一样。redis.call函数会将这5种类型的返回值转化对应的Lua的数据类型

从Lua脚本中获得返回值

在很多情况下我们都需要脚本可以有返回值,在脚本中可以使用return 语句将值返回给redis客户端,通过return语句来执行,如果没有执行return,默认返回为nil。

如何在redis中执行lua脚本

Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本。

[EVAL]  [脚本内容] [key参数的数量]  [key …] [arg …]

可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYSARGV 这两个类型的全局变量访问。比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:

lua脚本的内容为: return redis.call(‘set’,KEYS[1],ARGV[1])         //KEYS和ARGV必须大写

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world

EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0

eval "return redis.call(‘get’,’hello’)" 0

EVALSHA命令

考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要

  1. Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中
  2. 执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”

通过以下案例来演示EVALSHA命令的效果

script load "return redis.call('get','hello')"          将脚本加入缓存并生成sha1命令

evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0

我们在调用eval命令之前,先执行evalsha命令,如果提示脚本不存在,则再调用eval命令

lua脚本实战

实现一个针对某个手机号的访问频次, 以下是lua脚本,保存为phone_limit.lua

local num=redis.call('incr',KEYS[1])

if tonumber(num)==1 then

redis.call('expire',KEYS[1],ARGV[1])

return 1

elseif tonumber(num)>tonumber(ARGV[2]) then

return 0

else

return 1

end

通过如下命令调用

./redis-cli --eval phone_limit.lua rate.limiting:13700000000 , 10 3

语法为 ./redis-cli –eval [lua脚本] [key…]空格,空格[args…]

脚本的原子性

redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令必须等待脚本执行完以后才能执行。为了防止某个脚本执行时间过程导致Redis无法提供服务。Redis提供了lua-time-limit参数限制脚本的最长运行时间。默认是5秒钟。

当脚本运行时间超过这个限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性),而是返回BUSY的错误

实践操作

打开两个客户端窗口

在第一个窗口中执行lua脚本的死循环

eval “while true do end” 0

在第二个窗口中运行get hello

最后第二个窗口的运行结果是Busy, 可以通过script kill命令终止正在执行的脚本。如果当前执行的lua脚本对redis的数据进行了修改,比如(set)操作,那么script kill命令没办法终止脚本的运行,因为要保证lua脚本的原子性。如果执行一部分终止了,就违背了这一个原则

在这种情况下,只能通过 shutdown nosave命令强行终止

java代码

RedisManager.java

  1. import redis.clients.jedis.Jedis;
  2. import redis.clients.jedis.JedisPool;
  3. import redis.clients.jedis.JedisPoolConfig;
  4.  
  5. public class RedisManager {
  6.  
  7. private static JedisPool jedisPool;
  8.  
  9. static {
  10. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  11. jedisPoolConfig.setMaxTotal(20);
  12. jedisPoolConfig.setMaxIdle(10);
  13. jedisPool = new JedisPool(jedisPoolConfig, "120.79.174.118", 6379);
  14. }
  15.  
  16. public static Jedis getJedis() throws Exception {
  17. if (null != jedisPool) {
  18. return jedisPool.getResource();
  19. }
  20.  
  21. throw new Exception("Jedispool was not init");
  22. }
  23. }

  

RedisLock.java 简单实现分布式锁

  1. import java.util.List;
  2. import java.util.UUID;
  3. import redis.clients.jedis.Jedis;
  4. import redis.clients.jedis.Transaction;
  5.  
  6. public class RedisLock {
  7.  
  8. public String getLock(String key, int timeout) {
  9. try {
  10. Jedis jedis = RedisManager.getJedis();
  11. String value = UUID.randomUUID().toString();
  12. long end = System.currentTimeMillis() + timeout;
  13. while (System.currentTimeMillis() < end) {
  14. if (jedis.setnx(key, value) == 1) {
  15. // 锁设置成功,redis操作成功
  16. jedis.expire(key, timeout);
  17. return value;
  18. }
  19. if (jedis.ttl(key) == -1) {
  20. // 检测过期时间,没有设置则设置
  21. jedis.expire(key, timeout);
  22. }
  23. Thread.sleep(1000);
  24. }
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. return null;
  29. }
  30.  
  31. public boolean releaseLock(String key, String value) {
  32. try {
  33. Jedis jedis = RedisManager.getJedis();
  34. while (true) {
  35. jedis.watch(key);// watch
  36. if (value.equals(jedis.get(key))) {// 判断获得锁的线程和当前redis中存的锁是同一个
  37. Transaction transaction = jedis.multi();
  38. transaction.del(key);
  39. List<Object> list = transaction.exec();
  40. if (list == null) {
  41. continue;
  42. }
  43. return true;
  44. }
  45. jedis.unwatch();
  46. break;
  47. }
  48. } catch (Exception e) {
  49. e.printStackTrace();
  50. }
  51. return false;
  52. }
  53.  
  54. public static void main(String[] args) {
  55. String key = "aaa";
  56. RedisLock redisLock = new RedisLock();
  57. String lockId = redisLock.getLock(key, 10000);
  58. if (null != lockId) {
  59. System.out.println("获得锁成功");
  60. } else {
  61. System.out.println("获得锁失败");
  62. }
  63. String lockId2 = redisLock.getLock(key, 10000);
  64. if (null != lockId2) {
  65. System.out.println("获得锁成功");
  66. } else {
  67. System.out.println("获得锁失败");
  68. }
  69.  
  70. boolean ret = redisLock.releaseLock(key, lockId);
  71. if (ret) {
  72. System.out.println("释放锁成功");
  73. } else {
  74. System.out.println("释放锁失败");
  75. }
  76.  
  77. String lockId3 = redisLock.getLock(key, 10000);
  78. if (null != lockId3) {
  79. System.out.println("获得锁成功");
  80. } else {
  81. System.out.println("获得锁失败");
  82. }
  83.  
  84. boolean ret2 = redisLock.releaseLock(key, lockId3);
  85. if (ret2) {
  86. System.out.println("释放锁成功");
  87. } else {
  88. System.out.println("释放锁失败");
  89. }
  90. }
  91. }

  

LuaDemo.java  执行lua脚本

  1. import java.util.ArrayList;
  2. import java.util.List;
  3.  
  4. import redis.clients.jedis.Jedis;
  5.  
  6. public class LuaDemo {
  7. public static void main(String[] args) throws Exception {
  8. Jedis jedis = RedisManager.getJedis();
  9.  
  10. String lua="local num=redis.call('incr',KEYS[1])\n"+
  11. "if tonumber(num)==1 then\n"+
  12. " redis.call('expire',KEYS[1],ARGV[1])\n"+
  13. " return 1\n"+
  14. "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+
  15. " return 0\n"+
  16. "else\n"+
  17. " return 1\n"+
  18. "end";
  19.  
  20. List<String> keys=new ArrayList<>();
  21. keys.add("ip:limit:127.0.0.1");
  22. List<String> arggs=new ArrayList<>();
  23. arggs.add("6000");
  24. arggs.add("10");
  25. Object obj=jedis.eval(lua,keys,arggs);
  26. System.out.println(obj);
  27.  
  28. }
  29. }

  

LuaDemo2.java 通过sha摘要缓存lua脚本

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import redis.clients.jedis.Jedis;
  4.  
  5. public class LuaDemo2 {
  6. public static void main(String[] args) throws Exception {
  7. Jedis jedis = RedisManager.getJedis();
  8.  
  9. String lua="local num=redis.call('incr',KEYS[1])\n"+
  10. "if tonumber(num)==1 then\n"+
  11. " redis.call('expire',KEYS[1],ARGV[1])\n"+
  12. " return 1\n"+
  13. "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+
  14. " return 0\n"+
  15. "else\n"+
  16. " return 1\n"+
  17. "end";
  18.  
  19. List<String> keys = new ArrayList<>();
  20. keys.add("ip:limit:127.0.0.1");
  21.  
  22. List<String> arggs = new ArrayList<>();
  23. arggs.add("6000");
  24. arggs.add("10");
  25. // 通过sha摘要缓存lua脚本,减少网络传输,提高性能。(redis重启缓存的sha摘要会丢失)
  26. String sha = jedis.scriptLoad(lua);
  27. System.out.println(sha);
  28. Object obj = jedis.evalsha(sha, keys, arggs);
  29. System.out.println(obj);
  30.  
  31. }
  32. }

  

maven配置

  1. <dependency>
  2. <groupId>redis.clients</groupId>
  3. <artifactId>jedis</artifactId>
  4. <version>2.9.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.commons</groupId>
  8. <artifactId>commons-pool2</artifactId>
  9. <version>2.4.3</version>
  10. </dependency>

redis笔记2的更多相关文章

  1. 【Redis笔记(四)】 Redis数据结构 - list链表

    原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50573605 经过前面的介绍,我们学习了Redis中string字符串.hash ...

  2. redis笔记之一

    NoSQL简介 全称是Not Only SQL,泛指菲关系型数据库,它是通过键值对存储数据并且将数据存储在内存中.而像mysql,sql server这些通过关系表存数据的就叫关系型数据库 为什么需要 ...

  3. Redis 笔记 01:入门篇

    Redis 笔记 01:入门篇 ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ...

  4. redis笔记总结之redis介绍

    一.Redis介绍: redis的发展历史简单的理解为因为使用类似MySql这类关系型数据库不方便进而开发的开源的.轻量级的.非关系型的,直到现在一直不断完善的一款NoSql数据库.具体的介绍大家可以 ...

  5. Redis笔记(1)数据结构与对象

    1.前言 此系列博客记录redis设计与实现一书的笔记,提取书本中的知识点,省略相关说明,方便查阅. 2.基本数据结构 2.1 简单动态字符串SDS(simple dynamic string) 结构 ...

  6. 2020ubuntu1804server编译安装redis笔记(一)及报make test错误解决办法

    redis的大名我想大家都不陌生,今天在ubuntu server上进行编译安装,虽然apt也可以安装,但作为内存数据库,redis又是c开发的,编译安装,对机器的适应和性能更好. 安装笔记如下 第1 ...

  7. 2020ubuntu1804server编译安装redis笔记(三)启动服务和使用redis

    第一篇笔记记录了ubuntu1804server编译安装redis5,接下来要配置redis5了 网址:https://www.cnblogs.com/qumogu/p/12435694.html 第 ...

  8. redis笔记补充

    redis补充 这篇文章是redis入门笔记的补充. 1.info命令 用来显示服务的信息. info命令可以跟下面的选项: server: 关于 Redis 服务器的一些信息 clients: 客户 ...

  9. redis笔记

    redis字符串 : 存储基本的一个键值对. redis哈希 : Redis的哈希值是字符串字段和字符串值之间的映射,所以他们是表示对象的完美数据类型. 一个哈希表可以存在多个键值对,可对键值进行增删 ...

  10. Redis笔记,安装和常用命令

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/96.html?1455870708 一.redis简单介绍 redis是N ...

随机推荐

  1. 复习一下KVC

    一. 前言 KVC(Key Value Coding)是Cocoa框架为开发者提供的非常强大的工具,简单解释为:键值编码.它依赖于Runtime,在OC的动态性方面发挥了重要作用. 它主要的功能在于直 ...

  2. 多线程CGD调度组原理

    我们常用的GCD调度组方式 //GCD常用调度组写法 -(void)demo1{ //创建调度组和队列 dispatch_group_t group = dispatch_group_create() ...

  3. linux系统管理-输入输出

    目录 linux系统管理-输入输出 参数传递xargs linux系统管理-输入输出 重定向 将原本要输出到屏幕上的数据信息,重新定向到指定的文件中 运行程序,或者输入一个命令:默认打开4个文件描述符 ...

  4. Mysql中 instr与concat

    #INSTR(字符串, 子串),#返回值:第一个子串的索引-1#类似indexOf()#例如:SELECT INSTR('人民万岁,世界万岁','万')SELECT INSTR('人民万岁,世界万岁' ...

  5. vue操作select获取option值

    如何实时的获取你选中的值 只用@change件事 @change="changeProduct($event)" 动态传递参数 vue操作select获取option的ID值 如果 ...

  6. python线程类的start()和run()

    start()方法: 开始线程活动. 对每一个线程对象来说它只能被调用一次,它安排对象在一个另外的单独线程中调用run()方法,而非当前所处的线程,当该方法在同一个线程对象中被调用超过一次时,会引入R ...

  7. 新建全色或者resize(毫无价值,只是做记录)

    import glob import os,sys import shutil import numpy as np import cv2 import matplotlib.pyplot as pl ...

  8. SLAM中的非线性优化

    总结一下SLAM中关于非线性优化的知识. 先列出参考: http://jacoxu.com/jacobian%E7%9F%A9%E9%98%B5%E5%92%8Chessian%E7%9F%A9%E9 ...

  9. [LOJ 6288]猫咪[CF 700E]Cool Slogans

    [LOJ 6288]猫咪[CF 700E]Cool Slogans 题意 给定一个字符串 \(T\), 求一个最大的 \(K\) 使得存在 \(S_1,S_2,\dots,S_k\) 满足 \(S_1 ...

  10. Python爬取信息管理系统计算学分绩点

    试手登录了下我们学校的研究生信息管理系统,自动计算学分绩点 # -*- coding:utf-8 -*- import urllib import urllib2 import re import c ...