在分布式场景下,有很多种情况都需要实现最终一致性。在设计远程上下文的领域事件的时候,为了保证最终一致性,在通过领域事件进行通讯的方式中,可以共享存储(领域模型和消息的持久化数据源),或者做全局XA事务(两阶段提交,数据源可分开),也可以借助消息中间件(消费者处理需要能幂等)。通过Observer模式来发布领域事件可以提供很好的高并发性能,并且事件存储也能追溯更小粒度的事件数据,使各个应用系统拥有更好的自治性。
本文主要探讨了一种实现分布式最终一致性的解决方案——采用分布式锁。基于分布式锁的解决方案,比如zookeeper,redis都是相较于持久化(如利用InnoDB行锁,或事务,或version乐观锁)方案提供了高可用性,并且支持丰富化的使用场景。 本文通过Java版本的redis分布式锁开源框架——Redisson来解析一下实现分布式锁的思路。
Redis本身支持的事务
MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务的基础,MULTI, EXEC, DISCARD 和 WATCH 命令是Redis事务的基石。一个Redis事务允许一组Redis命令单步执行,并提供下面两个重要保证:一个事务中的所有命令串行执行;要么全部命令要么没有任何命令被处理。具体可参开这篇文章:http://blog.xiping.me/2010/12/transaction-in-redis.html。
事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
EXEC 命令负责触发并执行事务中的所有命令:
如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。当使用 AOF 方式做持久化的时候, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。然而,如果 Redis 服务器因为某些原因被管理员杀死,或者遇上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。
自己实现Redis分布式锁
根据一些分布式锁相关的文档,开始动手根据redis提供的原子操作进行实现:
在其中主要用到了Redis中的两条原子命令:
1.SETNX key value
Available since 1.0.0. Time complexity: O(1) Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists". Return value Integer reply, specifically: 1 if the key was set 0 if the key was not set
2.GETSET key value
起始版本:1.0.0 时间复杂度:O(1) 自动将key对应到value并且返回原来key对应的value,返回之前的旧值,如果之前Key不存在将返回nil。
摘自文章:http://www.jeffkit.info/2011/05/994/,其中有一节:GETSET的妙用,其中有一段:
上一个经验虽说可以解决这条数据该“插入还是更新”的问题,但需要知道当前操作是否针对某数据的首次操作的需求还不少。例如我的程序会在不同时间接收到同一条消息的不同分片信息包,我需要在收到该消息的首个信息包(发送是无序的)时做些特殊处理。 早些时候的做法是为消息在MongoDB维护一个状态对象,有信息包来的时候就走“上锁->检查消息状态->根据状态决定做不做特殊操作->解锁” 这个流程,虽然同事已经把锁的粒度控制得非常细了,但有锁的程序遇上多实例部署就歇了。 Redis的GETSET是解决这个问题的终极武器,只需要把当前信息包的唯一标识对指定的状态属性进行一次GETSET操作,再判断返回值是否为空则知道是否首次操作。GETSET替我们把两次读写的操作封装成了原子操作。
实现逻辑
获取redis针对某个锁的时候,需要根据lockKey区进行setnx(set not exist,顾名思义,如果key值为空,则正常设置,返回1,否则不会进行设置并返回0)操作,如果设置成功,表示已经获得锁,否则并没有获取锁。
如果没有获得锁,去Redis上拿到该key对应的值,在该key上我们存储一个时间戳(用毫秒表示,t1),为了避免死锁以及其他客户端占用该锁超过一定时间(5秒),使用该客户端当前时间戳,与存储的时间戳作比较。
如果没有超过该key的使用时限,返回false,表示其他人正在占用该key,不能强制使用;如果已经超过时限,那我们就可以进行解锁,使用我们的时间戳来代替该字段的值。使用getset来设置该字段的值,getset可以返回设置完成之前的值t2,如果t2!=t1,说明在getset之前已经有其他线程已经设置了该字段,事实上,我们还是没有获得该锁(已经被其他人抢占)。但是这会造成一定的数据错误,因为我们已经用getset设置了一个新的时间戳(并不是抢占该锁的客户端设置的时间戳),所幸这样操作的误差比较小可以忽略不计。
但是如果在setnx失败后,get该值却无法拿到该字段时,说明在我们操作之前该锁已经被释放,这个时候,最好的办法就是重新执行一遍setnx方法来获取其值以获得该锁。当然,这仍然可能失败,但失败的逻辑与之前的相同。虽然出现这种情况非常少见,但是为了避免每次都出现导致StackOverflowError的错误,设置调用层次。
public static final int ACQUIRE_LOCK_MAX_ATTEMPTS = 10; public static final long EXPIRE_TIME = 5000L; /** * 基于时间戳根据lockKey尝试获取锁,需要与releaseLock成对使用; * <p/>使用该方法的前提是必须要保证服务器之间时间同步 * <p/>如果持有锁的时间超过 #{EXPIRE_TIME},视为超时,其他客户端可以对其进行重新获取锁的操作 * * @param lockKey - 锁键值,即争夺的资源 * @return - 当成功获取锁后,返回true,否则返回false;如果没有获取锁,需要客户端进行轮询来尝试获取 */ public boolean acquireLock(String lockKey) { return acquireLock(lockKey, 0); } private boolean acquireLock(String lockKey, int depth) { long setnx = jedis.setnx(lockKey, String.valueOf(System.currentTimeMillis())); if (setnx == 1L) { //说明客户端已经获得锁 LOG.info(String.format("lock key : %s is acquired!", lockKey)); return true; } else { //此时,该lockKey已经被其他客户端加锁 String keyTimestamp = jedis.get(lockKey); if (keyTimestamp == null) { //如果该值已经被清空,就尝试去重新获取 if (depth == ACQUIRE_LOCK_MAX_ATTEMPTS) { //如果尝试次数超过10次,则不再尝试,直接返回false LOG.info(String.format("lock key : %s exceed max attemps: %s, quit!", lockKey, ACQUIRE_LOCK_MAX_ATTEMPTS)); return false; } return acquireLock(lockKey, depth + 1); } long intervalTime = System.currentTimeMillis() - Long.valueOf(keyTimestamp); if (intervalTime < EXPIRE_TIME) { //锁在一定时间内并没有超时,获取锁失败 LOG.info("lock key : %s acquire failed! other client persist this lock!", lockKey); return false; } else { //锁已经超时,尝试执行getset操作,设置当前时间戳 String getSetTimestamp = jedis.getSet(lockKey, String.valueOf(System.currentTimeMillis())); if (getSetTimestamp == null) { //考虑非常特殊的情况,有人释放了锁执行del操作 //此时get/set拿到的是nil值,说明已经获得了锁 LOG.info(String.format("lock key : %s is acquired! GETSET returns null!", lockKey)); return true; } if (!getSetTimestamp.equals(keyTimestamp)) { //在设置时,说明该锁已经被其他client加上 //此时会有对应的副作用,比如 LOG.info("lock key : %s acquire failed! other client acquire this lock!", lockKey); return false; } else { //锁已更新,可以正常返回 LOG.info(String.format("lock key : %s is acquired! origin client is time out!", lockKey)); return true; } } } }
释放锁的过程相对来说比较简单,但是我们采用这种方式的话,不能控制什么时候该释放锁,当然也可以采用回调的方式来实现默认释放掉锁,以便于控制释放过程。
/** * 释放lockKey对应的锁,注意需要与acquireLock成对使用 * <p/>不能随意对其他人使用的锁进行释放操作 * * @param lockKey * @return - 如果释放成功返回true */ public boolean releaseLock(String lockKey) { boolean result = jedis.del(lockKey) == 1L; LOG.info(String.format("lock key: %s is released!", lockKey)); return result; }
但这套毕竟是自己实现的,还会有很多漏洞,在github发现了一套开源的实现:https://github.com/mrniko/redisson/wiki,可以对其进行深入研究并应用到我们的系统中,这样系统会更加健壮。
在简单对其使用Jmeter进行性能测试,发现库存控制比较好,能够满足实际需求,但测试过程中也遇到了一些问题。
我们如果使用这种方式,能否让没有拿到锁的线程能够及时收到通知,重新连接?
对于使用的JedisClient时,不能每次都使用同一个实例,需要在必要的情况化对其执行回收。
四月 28, 2016 5:03:18 下午 org.apache.catalina.core.StandardWrapperValve invoke 严重: Servlet.service() for servlet [springmvc] in context with path [] threw exception [Request processing failed; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Broken pipe] with root cause java.net.SocketException: Broken pipe at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109) at java.net.SocketOutputStream.write(SocketOutputStream.java:153) at redis.clients.util.RedisOutputStream.flushBuffer(RedisOutputStream.java:52) at redis.clients.util.RedisOutputStream.flush(RedisOutputStream.java:213) at redis.clients.jedis.Connection.flush(Connection.java:288) at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:214) at redis.clients.jedis.Connection.getBulkReply(Connection.java:205) at redis.clients.jedis.Jedis.get(Jedis.java:101) at com.api.example.controller.UserController.decrElement(UserController.java:52) at sun.reflect.GeneratedMethodAccessor25.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:777) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:706) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857) at javax.servlet.http.HttpServlet.service(HttpServlet.java:620) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842) at javax.servlet.http.HttpServlet.service(HttpServlet.java:727) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:314) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745)
此时就需要使用JedisPool来获取资源,可以避免出现这个问题,注意在最后要回收资源:
Jedis jedis = jedisPool.getResource(); try { while (true) { String productCountString = jedis.get("product"); if (Integer.parseInt(productCountString) > 0) { if (acquireLock(jedis, "abc")) { int productCount = Integer.parseInt(jedis.get("product")); System.out.println(String.format("%tT --- Get product: %s", new Date(), productCount)); // System.out.println(productCount); jedis.decr("product"); releaseLock(jedis, "abc"); return "Success"; } Thread.sleep(1000L); } else { return "Over"; } } } finally { jedis.close(); }
相关推荐
自己整理的如何利用redis实现分布式锁,redis实现分布式锁看这一篇绝对够。
从业务场景出发,从抽象到实现阐述了如何利用redis实现分布式锁,完成简单的秒杀功能,也记录了笔者思考的过程,希望能给阅读到本篇文章的人一些启发。
锁的本质就是互斥,保证任何时候能有一个客户端持有同一个锁,如果考虑使用redis来实现一个分布式锁,最简单的方案就是在实例里面创建一个键值,释放锁的时候,将键值删除。但是一个可靠完善的分布式锁需要考虑的...
本项目是以学习的目的来一步一步实现一个最简单的基于Redis实现的分布式锁 简介 在分布式环境中,需要一种跨JVM的互斥机制来控制共享资源的访问 例如,为避免用户操作重复导致交易执行多次,使用分布式锁可以将某...
数据同步:基于redis的分布式锁。 Web安全:实现XSS过滤和CSR过滤。 多系统交互:Dubbo,ActiveMQ多系统交互。 前后端分离:前端使用ajax访问后端的rest服务,后端返回json格式数据。页面用nginx反向代理访问。 支付...
主要介绍了基于Redis的分布式锁的简单实现方法,Redis官方给出两种思路,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
最近在项目中遇到了类似“秒杀”的业务场景,在本篇博客中,我将用一个非常简单的demo,阐述实现所谓“秒杀”的基本思路。 业务场景 所谓秒杀,从业务角度看,是短时间内多个用户“争抢”资源,这里的资源在大部分...
基于redis实现的 基于zookeeper实现的 今天我们重点说一下基于redis的分布式锁,redis分布式锁的实现我们可以参照。 实现Redis分布式锁的最简单的方法就是在Redis中创建一个key,这个key有一个失效时间(TTL),以...
最近博主在看redis的...redis的分布式锁其实就是基于setnx方法和redis对key可设置有效时间的功能来实现的。基本用法比较简单。 public boolean tryLock(String lock,long expireTime){ String expire = String.va
感兴趣可以去Redisson官网看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。 一段简单的使用代码片段,先直观的感受一下: 是不是感觉简单的不行!此外,还支持Redis单实例、Redis...
本文实例讲述了PHP+redis实现的悲观锁。分享给大家供大家参考,具体如下: 锁机制 通常使用的锁分为乐观锁,悲观锁这两种,简单介绍下这两种锁,作为本文的背景知识,对这类知识已经有足够了解的同学可以跳过这部分...
Redis还具有多种应用场景,如在Web应用中存储会话信息、实现消息队列、排行榜和计数器、分布式锁以及实时分析等。例如,Redis的发布订阅功能可以用来实现消息队列,通过将消息发布到特定的频道,可以让订阅该频道的...
分布式锁DistributedLock是一个.NET库,它基于各种基础技术提供了健壮且易于使用的分布式互斥锁,读写器锁和信号灯。 使用DistributedLock,跨多个应用程序/机器同步对代码区域的访问非常简单: using ( await ...
RedisCache是基于Jedis的SDK。 ①强大的泛型支持,实现了任意java对象简单存\取 ②增强缓存接口:支持防穿透设计,以及本地缓存同步更新 ③提供多种分布式锁,分布式队列支持 ④个性化的redis源配置支持
博客中心是博客的核心,分页和轮滑加载均实现,用Redisson来实现分布式锁控制文章 搜索引擎中心:文章提示信息的增删改查,分页,高亮模糊排序查询 用户的消息中心:websocket聊天与用户的所有个人消息 用户的个人...
主要模块: 封装Redis LuaScript对象,方便在项目中直接定义lua脚本,并根据需要同步到需要使用的redis实例...基于Redis实现的毫米级定时器,使用生产者消费者模型,支持分布式部署; 开发计划: 模块 进度 上下文(Co
当然,Redis的的的还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件代替,并不是非要使用Redis的的的。因此,这个问题主要从性能和并发两个角度去答。性能:我们在碰到...
Hadoop Practice ...使用redis简单正确实现分布式锁 使用Lettuce作为redis客户端实例 redis持久化详解 Zookeeper Practice 基于zookeeper的服务注册与发现 基于zookeeper实现leader选举 zookeeper的leader选举机制