分布式锁

  1. setnx(set if not exists)

如果设值成功则证明上锁成功,然后再调用del指令释放。

  1. // 这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解
  2. > setnx lock:codehole true
  3. OK
  4. ... do something critical ...
  5. > del lock:codehole
  6. (integer) 1

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

  1. setnx(set if not exists) 加上过期时间
  1. > setnx lock:codehole true
  2. OK
  3. > expire lock:codehole 5
  4. ... do something critical ...
  5. > del lock:codehole
  6. (integer) 1

如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

  1. 使用ex nx命令一起执行
  1. > set lock:codehole true ex 5 nx
  2. OK
  3. ... do something critical ...
  4. > del lock:codehole
  1. 删除锁的线程必须是上锁的线程

为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其它线程释放,除非这个锁是过期了被服务器自动释放的。

但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于delifequals这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

  1. 上锁
  2. tag = random.nextint() # 随机数
  3. if redis.set(key, tag, nx=True, ex=5):
  4. do_something()
  5. redis.delifequals(key, tag) # 假想的 delifequals 指令
  6. # delifequals 解锁
  7. if redis.call("get",KEYS[1]) == ARGV[1] then
  8. return redis.call("del",KEYS[1])
  9. else
  10. return 0
  11. end

延时队列

消息队列

注意:

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用lpop 和 rpop来出队列。

  1. > rpush notify-queue apple banana pear
  2. (integer) 3
  3. > llen notify-queue
  4. (integer) 3
  5. > lpop notify-queue
  6. "apple"
  7. > llen notify-queue
  8. (integer) 2
  9. > lpop notify-queue
  10. "banana"
  11. > llen notify-queue
  12. (integer) 1
  13. > lpop notify-queue
  14. "pear"
  15. > llen notify-queue
  16. (integer) 0
  17. > lpop notify-queue
  18. (nil)

阻塞队列

如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。

通常我们使用 sleep 来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。但是有个小问题,那就是睡眠会导致消息的延迟增大。

我们可以使用 blpop/brpop,阻塞读。

阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用blpop/brpop替代前面的lpop/rpop,就完美解决了上面的问题。

锁冲突处理

上面我们讲了分布式锁的问题,但是加锁失败没有讲。一般我们有3种策略来处理加锁失败:

  1. 直接抛出异常,通知用户稍后重试

    这种方式比较适合由用户直接发起的请求,用户看到错误对话框后,会先阅读对话框的内容,再点击重试,这样就可以起到人工延时的效果。
  2. sleep 一会再重试

    sleep 会阻塞当前的消息处理线程,会导致队列的后续消息处理出现延迟。如果碰撞的比较频繁或者队列里消息比较多,sleep 可能并不合适。
  3. 将请求转移至延时队列,过一会再试

    这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。

延时队列

延时队列可以通过 Redis 的 zset(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset 的value,这个消息的到期处理时间作为score,然后用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。

  1. import java.lang.reflect.Type;
  2. import java.util.Set;
  3. import java.util.UUID;
  4. import com.alibaba.fastjson.JSON;
  5. import com.alibaba.fastjson.TypeReference;
  6. import redis.clients.jedis.Jedis;
  7. public class RedisDelayingQueue<T> {
  8. static class TaskItem<T> {
  9. public String id;
  10. public T msg;
  11. }
  12. // fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference
  13. private Type TaskType = new TypeReference<TaskItem<T>>() {
  14. }.getType();
  15. private Jedis jedis;
  16. private String queueKey;
  17. public RedisDelayingQueue(Jedis jedis, String queueKey) {
  18. this.jedis = jedis;
  19. this.queueKey = queueKey;
  20. }
  21. public void delay(T msg) {
  22. TaskItem<T> task = new TaskItem<T>();
  23. task.id = UUID.randomUUID().toString(); // 分配唯一的 uuid
  24. task.msg = msg;
  25. String s = JSON.toJSONString(task); // fastjson 序列化
  26. jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s); // 塞入延时队列 ,5s 后再试
  27. }
  28. public void loop() {
  29. while (!Thread.interrupted()) {
  30. // 只取一条
  31. Set<String> values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
  32. if (values.isEmpty()) {
  33. try {
  34. Thread.sleep(500); // 歇会继续
  35. } catch (InterruptedException e) {
  36. break;
  37. }
  38. continue;
  39. }
  40. String s = values.iterator().next();
  41. if (jedis.zrem(queueKey, s) > 0) { // 抢到了
  42. TaskItem<T> task = JSON.parseObject(s, TaskType); // fastjson 反序列化
  43. this.handleMsg(task.msg);
  44. }
  45. }
  46. }
  47. public void handleMsg(T msg) {
  48. System.out.println(msg);
  49. }
  50. public static void main(String[] args) {
  51. Jedis jedis = new Jedis();
  52. RedisDelayingQueue<String> queue = new RedisDelayingQueue<>(jedis, "q-demo");
  53. Thread producer = new Thread() {
  54. public void run() {
  55. for (int i = 0; i < 10; i++) {
  56. queue.delay("codehole" + i);
  57. }
  58. }
  59. };
  60. Thread consumer = new Thread() {
  61. public void run() {
  62. queue.loop();
  63. }
  64. };
  65. producer.start();
  66. consumer.start();
  67. try {
  68. producer.join();
  69. Thread.sleep(6000);
  70. consumer.interrupt();
  71. consumer.join();
  72. } catch (InterruptedException e) {
  73. }
  74. }
  75. }

redis分布式锁&队列应用的更多相关文章

  1. redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...

  2. Redis分布式锁的实现

    前段时间,我在的项目组准备做一个类似美团外卖的拼手气红包[第X个领取的人红包最大],基本功能实现后,就要考虑这一操作在短时间内多个用户争抢同一资源的并发问题了,类似于很多应用如淘宝.京东的秒杀活动场景 ...

  3. Redis分布式锁实现简单秒杀功能

    这版秒杀只是解决瞬间访问过高服务器压力过大,请求速度变慢,大大消耗服务器性能的问题. 主要就是在高并发秒杀的场景下,很多人访问时并没有拿到锁,所以直接跳过了.这样就处理了多线程并发问题的同时也保证了服 ...

  4. Redis分布式锁实战

    什么是分布式锁 在单机部署的情况下,要想保证特定业务在顺序执行,通过JDK提供的synchronized关键字.Semaphore.ReentrantLock,或者我们也可以基于AQS定制化锁.单机部 ...

  5. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  6. Redis分布式锁的正确姿势

    1. 核心代码: import redis.clients.jedis.Jedis; import java.util.Collections; /** * @Author: qijigui * @C ...

  7. 手撕redis分布式锁,隔壁张小帅都看懂了!

    前言 上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具.上一篇运用了Mysql中的select ...for update实现了 ...

  8. 循序渐进 Redis 分布式锁(以及何时不用它)

    场景 假设我们有个批处理服务,实现逻辑大致是这样的: 用户在管理后台向批处理服务投递任务: 批处理服务将该任务写入数据库,立即返回: 批处理服务有启动单独线程定时从数据库获取一批未处理(或处理失败)的 ...

  9. 利用redis分布式锁的功能来实现定时器的分布式

    文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...

随机推荐

  1. javaweb中Servlet配置到Tomcat

    1.tomcat容器来运行Servlet程序 在javase中,都是在控制台中运行java代码,而且提供了一个main方法,代码运行的入口.在javaee中,想要运行java代码,不是通过控制台程序来 ...

  2. Java接口中的成员变量默认为(public、static、final)、方法为(public、abstract)

    interface”(接口)可将其想象为一个“纯”抽象类.它允许创建者规定一个类的基本形式:方法名.自变量列表以及返回类型,但不实现方法主体.接口也可包含基本数据类型的数据成员,但它们都默认为publ ...

  3. JS中 【“逻辑运算”,“面试题:作用域问题”,“dom对象”】这些问题的意见见解

    1.逻辑运算 ||  &&  ! ||:遇到第一个为true的值就中止并返回 &&:遇到第一个为false的值就中止并返回,如果没有false值,就返回最后一个不是fa ...

  4. Atlassian In Action-Jira之指导思想(一)

    太上,不知有之:其次,亲而誉之:其次,畏之:其次,侮之.信不足焉,有不信焉.悠兮,其贵言.功成事遂,百姓皆谓"我自然". --<道德经> 研发管理或者系统工具的指导思想 ...

  5. HDU 2121:Ice_cream’s world II(不定根的最小树形图)

    题目链接 题意 求有向图的最小生成树,且根不定. 思路 最小树形图即求有向图的最小生成树,用的是朱刘算法. 这里不定根,那么可以建立一个虚根,让虚根和所有点相连,权值为一个很大的数(这里直接设为所有边 ...

  6. HDU 5775:Bubble Sort(树状数组)

    http://acm.hdu.edu.cn/showproblem.php?pid=5775 Bubble Sort Problem Description   P is a permutation ...

  7. CSU 1508:地图的四着色(DFS+剪枝)

    http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1508 题意:地图中四联通的块是一个国家,A和B每个人可以涂两种颜色,且B不能涂超过5次,相邻的国家 ...

  8. 谈谈 c# 对象初始化问题

    C#对象初始化 之前在学习过程中只是知道该如何初始化对象,但是却不明白为何要这么做,不这么做有什么问题. 现在就针对我最近遇到的问题(定义了全局字节数组没有初始化,然后在多线程里头使用,然后就一直报n ...

  9. 【转载】DOMContentLoaded与load的区别

    DOMContentLoaded与load的区别   (1)在chrome浏览器的开发过程中,我们会看到network面板中有这两个数值,分别对应网 络请求上的标志线,这两个时间数值分别代表什么? ( ...

  10. sql server编写通用脚本实现获取一年前日期的方法

    问题: 在数据库编程开发中,有时需要获取一年前的日期,以便以此为时间的分界点,查询其前后对应的数据量.例如:1. 想查询截止到一年前当天0点之前的数据量,以及一年前当天0点开始到现在的数据量.2. 想 ...