什么是分布式锁?几种分布式锁分别是怎么实现的?

<h3>推荐学习</h3> <h3><strong><a href="https://links.jianshu.com/go?to=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FBV19v411P7Cx%2F" target="_blank">分布式-全家桶(面试+技术):分布式锁+分布式事务+分布式缓存,redis+zk+nginx+mq+kafka等,必须死磕!</a></strong></h3> <div class="image-package"> <div class="image-container"> <div class="image-container-fill"></div> <div class="image-view" data-width="1046" data-height="651"><img data-original-src="//upload-images.jianshu.io/upload_images/24636137-6b36128b7dc16def.png" data-original-width="1046" data-original-height="651" data-original-format="image/png" data-original-filesize="48785" src="https://imgproxy.citrusreader.com/KDEXzrLmfOpZxN8IqL1lQhWRWdIJCaGvS_uD-6AuOSM/raw:1/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDYzNjEzNy02YjM2MTI4YjdkYzE2ZGVmLnBuZw" referrerpolicy="no-referrer"></div> </div> <div class="image-caption">image.png</div> </div> <h1>一、什么是分布式锁:</h1> <p><strong>1、什么是分布式锁:</strong></p> <p>分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是<strong>控制共享资源访问</strong>的问题,而分布式锁,就是解决了<strong>分布式系统中控制共享资源访问</strong>的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小&lt;typo id="typo-135" data-origin="粒度" ignoretag="true"&gt;粒度&lt;/typo&gt;从线程升级成了进程。</p> <p><strong>2、分布式锁应该具备哪些条件:</strong></p> <ul> <li>在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行</li> <li>高可用的获取锁与释放锁</li> <li>高性能的获取锁与释放锁</li> <li>具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)</li> <li>具备锁失效机制,即自动解锁,防止死锁</li> <li>具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败</li> </ul> <p><strong>3、分布式锁的实现方式:</strong></p> <blockquote> <p>基于数据库实现分布式锁基于Zookeeper实现分布式锁基于reids实现分布式锁</p> </blockquote> <p>这篇文章就简单介绍下这几种分布式锁的实现,重点讲解的是基于redis的分布式锁。</p> <h1>二、基于数据库的分布式锁:</h1> <p>基于数据库的锁实现也有两种方式,一是基于数据库表的增删,另一种是基于数据库排他锁。</p> <p><strong>1、基于数据库表的增删:</strong></p> <p>基于数据库表增删是最简单的方式,首先创建一张锁的表主要包含下列字段:类的全路径名+方法名,时间戳等字段。</p> <p>具体的使用方式:当需要锁住某个方法时,往该表中插入一条相关的记录。类的全路径名+方法名是有唯一性约束的,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。执行完毕之后,需要delete该记录。</p> <p>(这里只是简单介绍一下,对于上述方案可以进行优化,如:应用主从数据库,数据之间双向同步;一旦挂掉快速切换到备库上;做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;使用while循环,直到insert成功再返回成功;记录当前获得锁的机器的主机信息和线程信息,下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了,实现可重入锁)</p> <p><strong>2、基于数据库排他锁:</strong></p> <p>基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:</p> <pre><code>public void lock(){ connection.setAutoCommit(false) int count = 0; while(count &lt; 4){ try{ select * from lock where lock_name=xxx for update; if(结果不为空){ //代表获取到锁 return; } }catch(Exception e){ } //为空或者抛异常的话都表示没有获取到锁 sleep(1000); count++; } throw new LockException(); } </code></pre> <p>在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。获得排它锁的线程即可获得分布式锁,当获得锁之后,可以执行方法的业务逻辑,执行完方法之后,释放锁connection.commit()。当某条记录被加上排他锁之后,其他线程无法获取排他锁并被阻塞。</p> <p><strong>3、基于数据库锁的优缺点:</strong></p> <p>上面两种方式都是依赖数据库表,一种是通过表中的记录判断当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。</p> <ul> <li>优点是直接借助数据库,简单容易理解。</li> <li>缺点是操作数据库需要一定的开销,性能问题需要考虑。</li> </ul> <h1>三、基于Zookeeper的分布式锁</h1> <p>基于zookeeper临时有序节点可以实现的分布式锁。每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。 (第三方库有 Curator,Curator提供的InterProcessMutex是分布式锁的实现)</p> <p>Zookeeper实现的分布式锁存在两个个缺点:</p> <ul> <li>(1)性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。</li> <li>(2)zookeeper的并发安全问题:因为可能存在网络抖动,客户端和ZK集群的session连接断了,zk集群以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。</li> </ul> <h1>四、基于redis的分布式锁:</h1> <p>redis命令说明:</p> <p>(1)setnx命令:set if not exists,当且仅当 key 不存在时,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。</p> <ul> <li>返回1,说明该进程获得锁,将 key 的值设为 value</li> <li>返回0,说明其他进程已经获得了锁,进程不能进入临界区。</li> </ul> <p>命令格式:setnx lock.key lock.value</p> <p>(2)get命令:获取key的值,如果存在,则返回;如果不存在,则返回nil</p> <p>命令格式:get lock.key</p> <p>(3)getset命令:该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。</p> <p>命令格式:getset lock.key newValue</p> <p>(4)del命令:删除redis中指定的key</p> <p>命令格式:del lock.key</p> <h1>方案一:基于set命令的分布式锁</h1> <p><strong>1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁</strong></p> <p><strong>2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁</strong></p> <blockquote> <p>(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。</p> <p>(2)解决方案:设置锁超时时间</p> </blockquote> <p><strong>3、设置锁&lt;typo id="typo-2688" data-origin="超时" ignoretag="true"&gt;超时&lt;/typo&gt;时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间</strong></p> <blockquote> <p>(1)存在问题:</p> <p>setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。</p> <p>(2)解决方案:redis的set命令支持在获取锁的同时设置key的过期时间</p> </blockquote> <p><strong>4、使用set命令加锁并设置锁过期时间:</strong></p> <p>命令格式:set &lt;lock.key&gt; &lt;lock.value&gt; nx ex &lt;expireTime&gt;</p> <p>详情参考redis使用文档:<a href="https://links.jianshu.com/go?to=http%3A%2F%2Fdoc.redisfans.com%2Fstring%2Fset.html" target="_blank">http://doc.redisfans.com/string/set.html</a></p> <p><strong>(1)存在问题:</strong></p> <p>① 假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行&lt;typo id="typo-3121" data-origin="的" ignoretag="true"&gt;的&lt;/typo&gt;很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。</p> <p>② 随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。</p> <p><strong>(2)解决方案:</strong></p> <p>可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。但是,这样做其实隐含了一个新的问题,get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性</p> <p><strong>5、锁续期:</strong>(这种机制类似于redisson的看门狗机制,文章后面会详细说明)</p> <p>虽然步骤4避免了线程A误删掉key的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个<strong>守护线程</strong>,用来给快要过期的锁“续期”。</p> <blockquote> <p>① 假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。</p> <p>② 情况一:当线程A执行完任务,会显式关掉守护线程。</p> <p>③ 情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。</p> </blockquote> <h1>方案二:基于setnx、get、getset的分布式锁</h1> <p><strong>1、实现原理:</strong></p> <p>(1)setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向步骤(2)</p> <p>(2)get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向步骤(3)</p> <p>(3)计算新的过期时间 newExpireTime=当前时间+锁超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime</p> <p>(4)判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。</p> <p>(5)在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行del命令释放锁(释放锁之前需要判断持有锁的线程是不是当前线程);如果大于锁设置的超时时间,则不需要再锁进行处理。</p> <p><strong>2、代码实现:</strong></p> <p>(1)获取锁的实现方式:</p> <pre><code>public boolean lock(long acquireTimeout, TimeUnit timeUnit) throws InterruptedException { acquireTimeout = timeUnit.toMillis(acquireTimeout); long acquireTime = acquireTimeout + System.currentTimeMillis(); //使用J.U.C的ReentrantLock threadLock.tryLock(acquireTimeout, timeUnit); try { //循环尝试 while (true) { //调用tryLock boolean hasLock = tryLock(); if (hasLock) { //获取锁成功 return true; } else if (acquireTime &lt; System.currentTimeMillis()) { break; } Thread.sleep(sleepTime); } } finally { if (threadLock.isHeldByCurrentThread()) { threadLock.unlock(); } } return false; } public boolean tryLock() { long currentTime = System.currentTimeMillis(); String expires = String.valueOf(timeout + currentTime); //设置互斥量 if (redisHelper.setNx(mutex, expires) &gt; 0) { //获取锁,设置超时时间 setLockStatus(expires); return true; } else { String currentLockTime = redisUtil.get(mutex); //检查锁是否超时 if (Objects.nonNull(currentLockTime) &amp;&amp; Long.parseLong(currentLockTime) &lt; currentTime) { //获取旧的锁时间并设置互斥量 String oldLockTime = redisHelper.getSet(mutex, expires); //旧值与当前时间比较 if (Objects.nonNull(oldLockTime) &amp;&amp; Objects.equals(oldLockTime, currentLockTime)) { //获取锁,设置超时时间 setLockStatus(expires); return true; } } return false; } } </code></pre> <p>tryLock方法中,主要逻辑如下:lock调用tryLock方法,参数为获取的超时时间与单位,线程在超时时间内,获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。</p> <p>(2)释放锁的实现方式:</p> <pre><code>public boolean unlock() { //只有锁的持有线程才能解锁 if (lockHolder == Thread.currentThread()) { //判断锁是否超时,没有超时才将互斥量删除 if (lockExpiresTime &gt; System.currentTimeMillis()) { redisHelper.del(mutex); logger.info("删除互斥量[{}]", mutex); } lockHolder = null; logger.info("释放[{}]锁成功", mutex); return true; } else { throw new IllegalMonitorStateException("没有获取到锁的线程无法执行解锁操作"); } } </code></pre> <p><strong>存在问题:</strong></p> <p>(1)这个锁的核心是基于System.currentTimeMillis(),如果多台服务器时间不一致,那么问题就出现了,但是这个bug完全可以从服务器运维层面规避的,而且如果服务器时间不一样的话,只要和时间相关的逻辑都是会出问题的</p> <p>(2)如果前一个锁超时的时候,刚好有多台服务器去请求获取锁,那么就会出现同时执行redis.getset()而导致出现过期时间覆盖问题,不过这种情况并不会对正确结果造成影响</p> <p>(3)存在多个线程同时持有锁的情况:如果线程A执行任务的时间超过锁的过期时间,这时另一个线程就可以获得这个锁了,造成多个线程同时持有锁的情况。类似于方案一,可以使用“锁续期”的方式来解决。</p> <p><strong>前两种redis分布式锁的存在的问题</strong></p> <p>前面两种redis分布式锁的实现方式,如果从“高可用”的层面来看,仍然是有所欠缺,也就是说当 redis 是单点的情况下,当发生故障时,则整个业务的分布式锁都将无法使用。</p> <p>为了提高可用性,我们可以使用主从模式或者哨兵模式,但在这种情况下仍然存在问题,在主从模式或者哨兵模式下,正常情况下,如果加锁成功了,那么master节点会异步复制给对应的slave节点。但是如果在这个过程中发生master节点宕机,主备切换,slave节点从变为了 master节点,而锁还没从旧master节点同步过来,这就发生了锁丢失,会导致多个客户端可以同时持有同一把锁的问题。来看个图来想下这个过程:</p> <div class="image-package"> <div class="image-container"> <div class="image-container-fill"></div> <div class="image-view" data-width="380" data-height="420"><img data-original-src="//upload-images.jianshu.io/upload_images/24636137-bdef0f1af6af6168.png" data-original-width="380" data-original-height="420" data-original-format="image/png" data-original-filesize="39665" src="https://imgproxy.citrusreader.com/0ei_0hTG9ucgC6fvxN9CjzODavLCGc0uKVp_ksyNRWU/raw:1/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDYzNjEzNy1iZGVmMGYxYWY2YWY2MTY4LnBuZw" referrerpolicy="no-referrer"></div> </div> <div class="image-caption">image.png</div> </div> <p>那么,如何避免这种情况呢?redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案:RedLock,在方案三我们就来详细介绍一下。(备注:如果master节点宕机期间,可以容忍多个客户端同时持有锁,那么就不需要redLock)</p> <h1>方案三:基于RedLock的分布式锁</h1> <p>redLock的官方文档地址:<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fredis.io%2Ftopics%2Fdistlock" target="_blank">https://redis.io/topics/distlock</a></p> <p>Redlock算法是Redis的作者 Antirez 在单Redis节点基础上引入的高可用模式。Redlock的加锁要结合单节点分布式锁算法共同实现,因为它是RedLock的基础</p> <p><strong>1、加锁实现原理:</strong></p> <p>现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:</p> <p>(1)获取当前Unix时间,以毫秒为单位,并设置超时时间TTL</p> <blockquote> <p>TTL 要大于 正常业务执行的时间 + 获取所有redis服务消耗时间 + 时钟漂移</p> </blockquote> <p>(2)依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁,当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间TTL,这样可以避免客户端死等。比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁</p> <p>(3)客户端 获取所有能获取的锁后的时间 减去 第(1)步的时间,就得到锁的获取时间。锁的获取时间要小于锁失效时间TTL,并且至少从半数以上的Redis节点取到锁,才算获取成功锁</p> <p>(4)如果成功获得锁,key的真正有效时间 = TTL - 锁的获取时间 - 时钟漂移。比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s</p> <p>(5)如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了。</p> <blockquote> <p>设想这样一种情况:客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。</p> </blockquote> <p>(6)失败重试:当client不能获取锁时,应该在随机时间后重试获取锁;同时重试获取锁要有一定次数限制;</p> <blockquote> <p>在随机时间后进行重试,主要是防止过多的客户端同时尝试去获取锁,导致彼此都获取锁失败的问题。</p> </blockquote> <p>算法示意图如下:</p> <div class="image-package"> <div class="image-container"> <div class="image-container-fill"></div> <div class="image-view" data-width="581" data-height="580"><img data-original-src="//upload-images.jianshu.io/upload_images/24636137-8faa3f124b379f4e.png" data-original-width="581" data-original-height="580" data-original-format="image/png" data-original-filesize="70501" src="https://imgproxy.citrusreader.com/V5h5DicShz_6_66aEBTjHvBZ5nBCcYfoAzV7bp2XEmQ/raw:1/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDYzNjEzNy04ZmFhM2YxMjRiMzc5ZjRlLnBuZw" referrerpolicy="no-referrer"></div> </div> <div class="image-caption">image.png</div> </div> <p><strong>2、RedLock性能及崩溃恢复的相关解决方法:</strong></p> <p>由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。前面我们说的主从架构下存在的安全性问题,在RedLock中已经不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的,具体的影响程度跟Redis持久化配置有关:</p> <p>(1)如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;</p> <p>(2)如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒一次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;</p> <p>(3)为了有效解决既保证锁完全有效性 和 性能高效问题:antirez又提出了“延迟重启”的概念,redis同步到磁盘方式保持默认的每秒1次,在redis崩溃单机后(无论是一个还是所有),先不立即重启它,而是等待TTL时间后再重启,这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响,缺点是在TTL时间内服务相当于暂停状态;</p> <p><strong>3、Redisson中RedLock的实现:</strong></p> <p>在JAVA的redisson包已经实现了对RedLock的封装,主要是通过 redisClient 与 lua 脚本实现的,之所以使用 lua 脚本,是为了实现加解锁校验与执行的事务性。</p> <p><strong>(1)唯一ID的生成:</strong></p> <p>分布式事务锁中,为了能够让作为中心节点的存储节点获取锁的持有者,从而避免锁被非持有者误解锁,每个发起请求的 client 节点都必须具有全局唯一的 id。通常我们是使用 UUID 来作为这个唯一 id,redisson 也是这样实现的,在此基础上,redisson 还加入了 threadid 避免了多个线程反复获取 UUID 的性能损耗</p> <pre><code>protected final UUID id = UUID.randomUUID(); String getLockName(long threadId) { return id + ":" + threadId; } </code></pre> <p><strong>(2)加锁逻辑:</strong></p> <p>redisson 加锁的核心代码非常容易理解,通过传入 TTL 与唯一 id,实现一段时间的加锁请求。下面是可重入锁的实现逻辑:</p> <pre><code>&lt;T&gt; RFuture&lt;T&gt; tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand&lt;T&gt; command) { internalLockLeaseTime = unit.toMillis(leaseTime); // 获取锁时向5个redis实例发送的命令 return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command, // 校验分布式锁的KEY是否已存在,如果不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间) "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + // 如果分布式锁的KEY已存在,则校验唯一 id,如果唯一 id 匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + // 获取分布式锁的KEY的失效时间毫秒数 "return redis.call('pttl', KEYS[1]);", // KEYS[1] 对应分布式锁的 key;ARGV[1] 对应 TTL;ARGV[2] 对应唯一 id Collections.&lt;Object&gt;singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); } </code></pre> <p><strong>(3)释放锁逻辑:</strong></p> <pre><code>protected RFuture&lt;Boolean&gt; unlockInnerAsync(long threadId) { // 向5个redis实例都执行如下命令 return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // 如果分布式锁 KEY 不存在,那么向 channel 发布一条消息 "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end;" + // 如果分布式锁存在,但是唯一 id 不匹配,表示锁已经被占用 "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " + // 如果就是当前线程占有分布式锁,那么将重入次数减 1 "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + // 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,不删除 "if (counter &gt; 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " + // 重入次数减1后的值如果为0,则删除锁,并发布解锁消息 "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; "+ "end; " + "return nil;", // KEYS[1] 表示锁的 key,KEYS[2] 表示 channel name,ARGV[1] 表示解锁消息,ARGV[2] 表示 TTL,ARGV[3] 表示唯一 id Arrays.&lt;Object&gt;asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId)); } </code></pre> <p><strong>(4)redisson中RedLock的使用:</strong></p> <pre><code>Config config = new Config(); config.useSentinelServers() .addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389") .setMasterName("masterName") .setPassword("password").setDatabase(0); RedissonClient redissonClient = Redisson.create(config); RLock redLock = redissonClient.getLock("REDLOCK_KEY"); try { // 尝试加锁,最多等待500ms,上锁以后10s自动解锁 boolean isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS); if (isLock) { //获取锁成功,执行对应的业务逻辑 } } catch (Exception e) { e.printStackTrace(); } finally { redLock.unlock(); } </code></pre> <p>可以看到,redisson 包的实现中,通过 lua 脚本校验了解锁时的 client 身份,所以我们无需再在 finally 中去判断是否加锁成功,也无需做额外的身份校验,可以说已经达到开箱即用的程度了。</p> <p>同样,基于RedLock实现的分布式锁也存在 client 获取锁之后,在 TTL 时间内没有完成业务逻辑的处理,而此时锁会被自动释放,造成多个线程同时持有锁的问题。而Redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的特性,当锁即将过期还没有释放时,不断的延长锁key的生存时间。(具体实现原理会在方案四进行介绍)</p> <h1>方案四:基于Redisson看门狗的分布式锁</h1> <p>前面说了,如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在JAVA的Redisson包中有一个"看门狗"机制,已经帮我们实现了这个功能。</p> <p><strong>1、redisson原理:</strong></p> <p>redisson在获取锁之后,会维护一个看门狗线程,当锁即将过期还没有释放时,不断的延长锁key的生存时间</p> <div class="image-package"> <div class="image-container"> <div class="image-container-fill"></div> <div class="image-view" data-width="1035" data-height="323"><img data-original-src="//upload-images.jianshu.io/upload_images/24636137-af444596f5e7db12.png" data-original-width="1035" data-original-height="323" data-original-format="image/png" data-original-filesize="50473" src="https://imgproxy.citrusreader.com/zqFEZFADJCTj_Jk-476v2Ulz-0Rn_ng-ZCtjB6qZB_s/raw:1/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDYzNjEzNy1hZjQ0NDU5NmY1ZTdkYjEyLnBuZw" referrerpolicy="no-referrer"></div> </div> <div class="image-caption">image.png</div> </div> <p><strong>2、加锁机制:</strong></p> <p>线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。</p> <p>线程去获取锁,获取失败:一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。</p> <p><strong>3、watch dog自动延期机制:</strong></p> <blockquote> <p>看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。</p> </blockquote> <p>redisson在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的1/3处,如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是30秒,可以通过 lockWactchdogTimeout 参数来改变。</p> <p>加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。</p> <p>那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了呗。</p> <p><strong>4、redisson分布式锁的关键点:</strong></p> <blockquote> <p>a. 对key不设置过期时间,由Redisson在加锁成功后给维护一个watchdog看门狗,watchdog负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效</p> <p>b. 通过Lua脚本实现了加锁和解锁的原子操作</p> <p>c. 通过记录获取锁的客户端id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。</p> </blockquote> <p><strong>5、Redisson的使用:</strong></p> <p>在方案三中,我们已经演示了基于Redisson的RedLock的使用案例,其实 Redisson 也封装 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等,具体使用说明可以参考官方文档:Redisson的分布式锁和同步器</p> <p>附:redLock的官方文档翻译</p> <div class="image-package"> <div class="image-container"> <div class="image-container-fill"></div> <div class="image-view" data-width="1430" data-height="6670"><img data-original-src="//upload-images.jianshu.io/upload_images/24636137-c101a541ee090a8d.png" data-original-width="1430" data-original-height="6670" data-original-format="image/png" data-original-filesize="1686035" src="https://imgproxy.citrusreader.com/g1hsH45OpM8LI3vCaxnwzLG-E3rQ4NWXPhoVpJqJERg/raw:1/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDYzNjEzNy1jMTAxYTU0MWVlMDkwYThkLnBuZw" referrerpolicy="no-referrer"></div> </div> <div class="image-caption">image.png</div> </div> <blockquote> <p>作者:张维鹏<br> 原文链接:<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fblog.csdn.net%2Fa745233700%2Farticle%2Fdetails%2F88084219" target="_blank">https://blog.csdn.net/a745233700/article/details/88084219</a></p> </blockquote>

2026/9/23
阅读更多

注重养生

<p>郑重声明:原创首发,文责自负。</p><p>今天中午,突然想吃凉皮了。</p><p>走进一家米皮店,点了一份凉米皮,一个肉夹馍。</p><p>隔壁桌的小伙子点了一份黑擀面皮加白面皮的组合,我们这里方言叫白加黑。听他的口音是外地人,好像是第一次吃,对味道赞不绝口。</p><p>“你喝稀饭不?”店主问。</p><p>“不用,谢谢!”小伙子说。</p><p>“免费赠送,不要钱的。”店主又说。</p><p>“不喝稀饭了,控糖。”小伙子笑着说。</p><p>店主没再说什么,去后厨忙碌了。</p><p>我们这里的小吃店,不管吃什么种类的凉皮,都会免费送一碗稀饭。如果单买稀饭,一块钱一碗。现在大家都注重养生,不怎么喝稀饭。于是,许多店里的稀饭都剩下了。</p><p>那次,与深圳的小伙伴聊天,得知他们也开始注重养生了。当年,我们经常一起拼酒,每次都喝的酩酊大醉,却不肯善罢甘休,不愿意服输。如今,他们也不喝酒了,甚至连饮料都不喝了,只喝茶水。大家都知道,控糖才是最重要的事情。</p>

2026/9/23
阅读更多

咖啡遇上茶

<p>郑重声明:本文为原创首发,文责自负。</p><p>“宝,今晚我不回来吃饭,公司要加班。我给你点了珍宝阁的外卖,先对付对付,明晚我再给你做。”林林看了一眼微信上的文字,她以为她会和以前一样,会暴跳如雷声嘶力竭地在微信上发一长段语音,命令他立即回来给她做饭。</p><p>今晚看到后很平静,平静得连自己都感到奇怪。也是,自己嫁的是丈夫,不是请保姆。他有他的生活和事业,不可能一直把他拴在裤腰带上,每天围在灶台转。</p><p>不过,林林感到很奇怪,老公王其军从结婚到现在,他每天晚上都会回来给她做饭。她已经习惯了他围着她转的感受,就像左手握上右手一样自然,现在突然不回来,让她非常难受。</p><p>可是,今晚是他这个月第三次没回来给她做饭了,实在让她难以接爱。站起来,拿上车钥匙,开门开车来到公司楼下,把车停在车库出口,打了一个电话,立即有一个穿着保安制服的中年男人跑过来,没有说话,打开旁边的一辆车坐了上去,按了下喇叭,林林跟在后面,一前一后驶进了夜色中。</p><p>最后,他们停在一家灯光灿烂的KTV门口,中年保安下车和门童说了几句话,门童点了点头走了进去后,中年保安开车走了。林林停好车,坐在车内看着KTV门口。</p><p>门童很快就走了出来,看到林林的车后走了过来,敲了敲车窗。林林打开窗户,门童把一个牛皮纸信封扔了进来。林林看了一眼信封,没有说话,开车回了家。</p><p>林林拿着信封回家后,她并没有急着去看信封中的东西。她平时是很少喝茶的,今晚特意沏了一壶陈年普洱。她学着老公的样子,把茶缓缓倒进紫砂茶杯中,茶色由浅入深,在茶室柔和的灯光下,通透明亮。</p><p>她端起茶杯送到唇边,最后还是放了下来。林林不喜欢喝茶,觉得喝茶太土了,没有咖啡高档。因此,她喜欢喝咖啡,喜欢用咖啡匙轻轻搅拌咖啡的优雅。坐在窗子前,夕阳的余晖透过玻璃窗,照在她搅动咖啡的手,真是美极了。</p><p>但在这一点上,林林和丈夫完全是两个世界的人。丈夫王其军嗜茶如命,用他的话说,不吃饭可以,但不喝茶是万万不能的。现在他应该是找到陪他喝茶的人了,林林想。</p><p>想到此,林林拿起茶几上的信封,回到了卧室。</p><p>卧室里床的正对面挂着他们的婚纱照,照片上的她美丽动人,他英俊潇洒。那时,她笑得真好看,幸福地依偎在他的胸前。</p><p>林林想起刚结婚时两人是多么的幸福,她看他柔情似水,他看她温柔体贴。她喝她的咖啡,他泡他的茶,虽然口味不一样的,幸福却是一样的。</p><p>后来,他们有了小宝宝,开始了手忙脚乱的生活,她喝咖啡时没有了原来的清香全是苦涩,他喝茶时同样,也是苦滋味。当初幸福哪去了,为什么剩下的全是生活的苦?他还爱她,这是她能感受到的,她也还爱他,这是心里明明白白的事。</p><p>为什么两个相爱的人感受不到生活的幸福呢?她和他谈过,他们说了很多,彼此都做了承诺。可是,下班后她只想待在咖啡馆,他干脆以加班的名义留在公司喝茶。好像家中有洪水猛兽一样,都不想回来。</p><p>他们以为是孩子影响了他们的二人世界,他做通父母的工作,直接把孩子送了父母家。刚开始时还好,两人都能准时回家,他泡他的茶,她喝她的咖啡,两人像熟悉的陌生人一样,一个晚上几乎是一句话都没说。慢慢地,又回到了从前,他待他的办公室,她泡她的咖啡馆。</p><p>她开始疑神疑鬼了,难道办公室比家里还好吗?他虽然每晚还给她做收,但越来越应付了。记得有一天晚上她给他打电话时,听到电话里有女人说话的声音,虽然当时没有刨根问底地追问那女人是谁。但在他回家后,她还情绪激动地要他解释清楚。</p><p>他看着她,脸上一副不可理喻的神色,淡淡他地回了一句,说是他的助理。接着无论她怎么问,他都是一言不发,让她千斤的大锤打在棉花上,想吵都不吵起来。</p><p>怀疑的种子一旦种下,很快就会发芽,开花,结果。她找到公司的保安队长,让他注意他的行踪。她父亲对保安队长有知遇之恩,因此对她很忠心。今晚看到他离开公司后第一时间就通知了她,并让KTV的熟人拍下了他在KTV包房的照片。</p><p>她想,先把这些牛皮袋信封装着的证据藏起来,或许以后有用。</p><p>正在这时,林林的电话响了,是她的咖友小王打来的。她顺手拿起电话接通,“喂”了一声。</p><p>“林姐,我朋友从巴西给我捎了咖啡豆,现在有时间吗,快到我家里,我请你尝尝鲜。”小王在电话里兴奋地说道。</p><p>“有时间。”林林想起KTV包房中的老公,想都没想地就答应了。可是当她刚一说完就后悔了,要是自己也晚上随随便便到别的男人家去品尝咖啡,那和去KTV喝茶的老公又有什么区别。于是急忙拒绝道:“不用了,今天太晚,我要休息了。”</p><p>“林姐,现在才十点,怎么能说太晚呢?”小王约过林林几次,每天可以,上咖啡馆可以,晚上不行,去家里更不行。好不容易今晚林林说有空,怎么能白白错过这个机会?小王劝道,“林姐,我咖啡豆都磨好了,你一到就可以喝。快来吧,你总不能辜负了我的好一片好心吧!我们这么多年的朋友难道你还不相信我?”</p><p>道德绑架!果然,男人讨好女人都是有目的的。现在是晚上十点,不是白天十点,邀请一个女人到家里喝咖啡,不管发不发生什么,对女人的名节都不是一件好事。林林虽然不想撕破脸,但还是不冷不热地再次拒绝道:“算了,下次我请你。”</p><p>“好吧。”小王想,她松了第一次口,以后有的是机会,要是把她逼得狠了,反而不美。</p><p>林林挂断电话,看了一眼手里的牛皮信封袋,突然心血来潮,有了一种打开的冲动。她走回茶室,重新倒了一杯刚才泡好的茶,把杯举到唇边,浅浅地尝了一下,好苦啊。她把茶吐了出来,又开了一瓶矿泉水,猛灌一口,才压住了口腔中的苦味。</p><p>既然已经坐下来了,那就打开信封看看吧,林林想。刚解开封口的细绳,门口传来密码开锁的声音。</p><p>是老公回来了。大门的密码除了双方父母,就只有她和老公知道。这个时候不可能是父母,只能是老公回来了。总不能当着老公看对老公不利的证据吧。林林急忙把信封塞到沙发底下,等方便的时候再看。</p><p>果然,是王其军回来了。见茶室里有光,走了过来。看到是林林坐在茶几前泡茶,惊喜地问道:“宝,你也开始喝茶了?”</p><p>“喝茶?喝什么茶,我在喝马尿。”林林想起老公骗自己加班去了KTV的事,气不打一处来,没好气地说。</p><p>“嗨嗨,我先去冲凉了。”老公不敢再说,蹑手蹑脚走进了洗手间,怕弄出声音再受老婆的怼。</p><p>林林觉得没意思,也不去看证据了,关了灯回房睡觉。那一晚,老公没有回房,在书房里搭了架行军床。</p><p>林林躺在床上辗转反侧睡不着,她不知道她们还能凑合多久。她也知道老公是爱她的,至少在今晚以前。她想起刚结婚时,老公在茶室泡茶时也会顺便给她冲杯手磨咖啡。</p><p>当时她还笑老公的手法比咖啡馆的小哥还要专业,可是生完孩子后,家里一切都乱了套,四个老人和两个年轻人都围着那个小不丁转,哄的哄,抱的抱,一个个手忙脚乱。</p><p>自己喝咖啡的时间少了,好像对爱喝茶的老公没什么影响。因为双方父母都是茶的拥趸,每天不是她亲爸在茶室里变着花样泡茶,就是公爹在钻研着茶经。</p><p>一有闲工夫,三个男人就在茶室里表演着自己的茶艺。有时两位母亲也会凑热闹,对他们的茶艺点评一番。那笑声,本来是爽快的,听在林林耳朵里,比任何嗓音都要刺耳。</p><p>林林提醒老爸,让他们在谈论茶道时小声点。老爸像看怪物一样看看她,笑着说她学什么小资喝咖啡,喝茶才是正道。</p><p>老公虽然经常和他们一起谈论茶道,也会时不时地给她泡上一杯咖啡,让她过过嘴瘾。在正常情况下,她应该是感激的。可一看到他们五个人一起亲密无间地谈论茶道,心里就有一股怒火直向脑门冲。喝到嘴里的咖啡也不香了,对茶更是深恶痛绝。</p><p>你有人陪着喝茶,难道我就不能有人陪着品尝咖啡。慢慢地,林林的朋友圈扩大了,有了一批喜欢喝咖啡的同志。</p><p>小王就是这个圈子里的一个。不过,林林对小王并没有太多的好感,她认为这人的脑子太灵光了,做的事说的话仿佛先前排练过一样,让人觉得太舒服了。而且他那双眼睛总是喜欢往女同胞的胸前瞟。这也是今晚她拒绝去他家里品尝咖啡的重要原因。</p><p>自己不是很讨厌茶吗?刚才自己喝了茶,虽然用矿泉水压过茶气,但现在自己并没有感到哪里不适,好像还有一种口腔回甘的味道。林林想,难道自己的本性不讨厌茶,只是自己给自己套了层壳,才觉得咖啡才是提神的最佳神器。不去想了,先睡吧。明天,明天一定要喝一杯茶,哪怕是喝后吐得天翻地覆,也要去尝试一下。</p><p>第二天是周末,林林睡到自然醒。她有一个习惯,只要不上班,她是不会吃早餐的,躺在床上刷手机到午餐时间,才慢悠悠起床洗漱。老公在家的话老公做饭,老公不在家就看冰箱有什么吃什么,速冻饺子,面条,甚至先天吃剩的剩饭剩饭都可以对付一餐。</p><p>用她自己的话说,累了一周还不好好休息休息,少吃一顿饭又不会死。吃完中餐后接着睡,睡到下午五点再起床洗漱,素面朝天去买菜。晚上是不能马虎的,该祭祭自己的五庄庙。</p><p>今天不一样,林林刚醒过来,小王的电话就打了进来,林林眯着眼睛接通了电话,懒懒散散地“喂”了一声。</p><p>“林姐,你醒了。”手机里传来小王兴奋的声音,“我现在开车过来接你,怎么样?”</p><p>“不怎么样。你别过来,我要睡觉。”林林想睡觉的时候,不要说喝咖啡,就是有满汉全席她都不会多看一眼,她的朋友都知道她这个习惯,在周末时除非有千金大事,一般都不会约她的。小王也知道,因此她毫不犹豫地拒绝了。</p><p>“这样啊。”小王有点失落,不过还是挂了电话。</p><p>小王的一通电话,搅得林林的睡意全无,刷手机也集中不了精神。算了,今天破个例,上午不睡了。</p><p>于是,林林爬起了床。她习惯性地走进茶室,想冲杯咖啡提提神。老公已经出去了,她去茶室看了下,昨晚泡过茶后的茶具老公收拾得干干净净。</p><p>她打开罐子准备取咖啡豆时,一时心血来潮,想起昨晚的决定,又一次坐到了茶几前。</p><p>她学着老公泡茶的样子,按了下热水壶的自动上水开关,开始烧开水。这次她打开罐子是铁观音茶罐,趁着烧开水的空档,准备用竹夹子夹茶叶。想了一下好像不对,老公是用开水洗过茶壶后才放茶叶的,那就再等会儿吧。</p><p>她拿着紫砂壶,看着壶壁的翠竹图案,古色古香,觉得比咖啡壶的历史底蕴厚重多了,难怪老公他们那么喜欢喝茶。</p><p>水开了,她把水倒进紫砂壶中,直到水从壶口漫出来,浇过壶壁。再把壶中的水全部倒进准备好的茶杯中,然后倒干净茶杯中的水。</p><p>她夹起茶叶放进砂壶中,用开水泡好。头泡不是用来喝的,还是用来洗茶杯。泡好第二泡后,终于可以喝茶了,她望着茶杯,感觉泡茶的仪式感满满,就算不喝,也是很有意义的。她举起茶杯,最终还是没有喝下去。</p><p>这样一来,喝咖啡的兴趣全被泡茶的仪式感冲跑了。林林拿起一包以前剩下的速浓咖啡,冲在开水中喝了。</p><p>小王的电话又来了,他在电话中焦急地说道:“林姐,有句话我不知道该不该说,是关于你老公的?”</p><p>“不知该不该说就别说。”林林最烦用这种话术引人说话的,用她爸的话说她就是一根肠子到底的人,心中一点点弯弯绕绕都没有,是一个傻白甜。不过林林的想法很现实,傻白甜好,没心计,人不容易老。</p><p>“林姐,刚才我看到你老公去KTV了。”小王还是主动说了出来。</p><p>“去KTV?没事,他一个大男人,有自己的生活正常。”</p><p>“好像还有个女人,是挽着他胳膊进去的。”小王想,我就不相信你不吃醋。</p><p>“是好像,就是没看清楚,他回来后我问他。”林林说,“另外还有事吗?”</p><p>“没有了。”</p><p>“没有了就挂了。”昨晚小王的电话,林林只觉得他没有边界,一个单身男人晚上约结了婚的女人去家里喝咖啡,不是自己送上门去让人说闲话的吗?今天早上的电话,也只是让人觉得他献殷勤献得太勤了。现在这个电话拿没看清楚的事来说,明显是别有所图了。自己只是在朋友的介绍下和你喝过几次咖啡,你这样关心人家的家事,是不是存心不良,因此,在内心里,林林觉得小王这人不可深交。</p><p>老公是不是真的带着别的女人去KTV了?这个问题似根刺一样刺着林林的心。她想来想去,还是给保安队长打了一个电话,问老公在不在公司。</p><p>保安队长告诉她,王总带着助理去见客户了。</p><p>林林知道老公的助理是女的,难道他们真的是去KTV了吗?</p><p>不去理他,他想去就去吧。反正都起床了,干脆趁周末去看看儿子。林林拿起车钥匙,开车去了公公家。</p><p>公公退休前是央企中层,现在住在单位的老式职工房中。房子看着简单,不过被婆婆布置得很温馨。她进门时,儿子看到她后立即光着脚丫跑了过来,伸着手要她抱抱。</p><p>“妈,你给我生个姐姐吧,妈妈。”儿子奶声奶气地说。</p><p>“生个姐姐?”</p><p>儿子点点头。</p><p>“小宝宝看到邻居小胖有个姐姐总是护着小胖,觉得很有派面,每天幻想着自己也有个姐姐就好了。”婆婆解释说。</p><p>“妈,你说行不行嘛?”儿子摇着她的肩膀说。</p><p>“好,我给你生个姐姐。”林林知道,不答应的话,儿子会把她的肩膀摇下来。</p><p>“好,妈妈真好,我也有姐姐了。”儿子从林林身上跳了下来,拍着手去告诉爷爷。</p><p>林林看着儿子兴奋的模样,心中是醋海微波,以前和老公虽然在喝茶和咖啡上有分歧,但双方互相包容,我喝我的咖啡他喝他茶,而且老公手脚麻利,做什么都快,有时在泡茶的空档还会给自己磨杯咖啡。现在呢?现在老公都会带着别的女人去KTV包房了,这婚姻,不知还能撑多久。</p><p>“林林来了,快坐。”公公抱着孙子走出茶室,冲婆婆说,“老婆子,林林来了,中午多做几个拿手菜,到时给其军打个电话,让他也回来,一家人有很长时间没一起好好吃饭了。”</p><p>“好咧。”婆婆答应一声,围上围裙走进了厨房。</p><p>公公和婆婆对她是真的好,比亲妈对她还要好,不是想到这些,她早就把老公在KTV的照片甩到他的脸上,把他赶出家门了。</p><p>公公把孙子放在沙发上,走进茶室,很好就端着一杯咖啡走了出来,味道是她最喜欢的味道,咖啡杯也是她最喜欢的品牌和颜色。</p><p>“林林,来,喝杯咖啡提提神,先休息会儿,我给王其军打过电话了,他说半小时就能到家。”公公把咖啡放在林林面底的矮桌上,告诉她已经给王其军打过电话了。</p><p>这个咖啡杯林林认识,是她第一次以王其军女朋友的身份上门时,公公按着她的喜好特意买的。不仅有手动磨豆机,咖啡杯和咖啡匙,连电子秤,温度计,咖啡分布器,咖啡压粉器,滤网,咖啡搅拌棒,牛奶拉花杯,咖啡渣敲击盒,咖啡清洁刷都买了。</p><p>林林当时看到这一整套用具,甚至觉得比咖啡馆都还要齐全。那天的咖啡是王其军冲的,手艺不咋的,但她觉得味道比星巴克的还要正宗。当她回家和老爸老妈说了这件事后,老爸老妈看王其军的眼神比看亲儿子还要亲。</p><p>让她觉得假如她不嫁的话,最先急的应该是老爸老妈。现在王其军冲咖啡的手艺提高了,但两人的心好像离得更远了。不管他,只要他不提出离婚就将就过吧!</p><p>“宝,你回家怎么不提前提前和我说声?我可以直接去接你啊。”王其军没说假话,半个小时后准时回了家,刚进门就对林林说道。</p><p>林林没有说话,只是冲他点点头。心想,一边口中喊着宝,一边和别的女人去KTV包房,你的演技实在是太好了。虽说看不破不说破,但我也不能当傻子,既然你想脚踏两只船,我也不能不提醒你。林林看了看王其军后面,问道:“你的助理呢,怎么不和你一起家?”</p><p>“我助理?跟我回家?宝,你是不是误会什么了?”</p><p>“没误会啊,你今天不是和你的助理一起去见客户了吗?你不留她回家吃个饭。”林林看王其军脸上一副波澜不惊的样子,不得不佩他能沉得住气。心中更加酸楚,看样子他们之间,无论做什么事都已经习以为常了。真后悔当初一再求老爸把他推上去,现在他上去了,自己的老公却走丢了。</p><p>“哦,你说的是刘助理,她拜访完客户就坐出租回家了。”</p><p>“哦。”林林没有再说,你高兴就行,你想走,就好聚好散吧,只求签离婚协议时彼此都保留一点体面。</p><p>“爸爸,”儿子听到开门声,看到是爸爸来了,立即从爷爷身上爬了下来,开心地对王其军说,“爸爸,妈妈答应给我生个姐姐。”</p><p>“姐姐,什么姐姐?”王其军不解地望着林林。</p><p>林林没理他,自顾自开进厨房,看能不能帮到婆婆。</p><p>“不用,林林,你去坐,帮忙的话我会叫军军的。他一个大男人不就是用来做苦力的吗?你好不容易回来一趟,生着等吃饭好了。”婆婆把林林推出厨房,偷偷冲儿子使了个眼色,王其军会意地走了进去。</p><p>“你们两人是不是有事,今天林林的脸色有点不对劲。”婆婆低声问道。</p><p>“没有啊。”</p><p>“真的?”</p><p>“妈,我们之间会有什么呢。妈,你放心吧,我会好好守护住你的宝贝儿媳妇的。”</p><p>“反正你小心点,你要是敢对不起林林,我就没有你这个儿子。”</p><p>母子俩嘴上说话,手上也没闲着。菜很快就炒好了,全是林林喜欢吃的。当菜全部搬上桌子时,林林的心都化了,有这么好的公公婆婆,就是自己受点委屈也算不得什么。</p><p>感动是感动,林林的筷子也没闲着,美味当前,不来个风卷残云真对不起自己傻妞儿的小名。公公婆婆爱怜地看着他,王其军偶尔还会把她夹不到的菜移到她面前来。</p><p>至于儿子,有公婆在,这都不是她应该操的心,她要的是努力干饭,对得住婆婆的手艺。</p><p>吃完饭,王其军已经把咖啡摆到了她面前,不是一杯,是两杯。</p><p>“我一杯就好了。”</p><p>“另一杯不是给你的,是给我自己的。”</p><p>“你不是不喝咖啡吗?”</p><p>“不喝也可以练啊,我都试着喝了一个月。”</p><p>“这还能练?你这段时间说加班是在练咖啡?”</p><p>婆婆点了点头,告诉林林,自从她嫌老公不陪她喝咖啡开始,王其军就一直在尝试,看怎样才能和她一样,能喝出咖啡的妙处来。经过几个月的苦练,终于能够和林林一样,也爱上了咖啡。</p><p>“真的,”林林问王其军。</p><p>“真的。”他端起杯子,用咖啡匙轻轻地搅拌着,咖啡随着小匙转起一个个优雅的圈子。然后,用小匙盛起咖啡,缓缓送到唇边。</p><p>这画面,真是太唯美了,看得林林都惊呆了。她第一次知道,品尝咖啡,不仅是用感官,还有那唯美的动作。她想不明白的是,一个曾经只喝茶的人能把咖啡喝得如此有美感,真不知费了多少功夫。</p><p>林林想起她泡过的那两次茶,顿时觉得茶也没有那么讨厌了。她看到公公正在茶室中泡茶,她放下杯子,走进茶室中,对公公说:“爸,给我倒杯茶。”</p><p>“好。”公公没有太多的迟疑,用开水洗过茶杯后,把茶倒进杯子中。</p><p>林林端进茶杯,感到有点烫,用嘴吹了吹,然后一饮而尽。</p><p>好苦啊,她想去倒杯水压压,脚刚提起就停了下来。不行,我就不相信一个从不喝咖啡的人能喝咖啡,我一个只喝咖啡的人不能喝茶。</p><p>奇怪,最初的苦味变淡了,慢慢满口生津,喉咙深处渐渐传来一股甜味。紧接着茶汁入肚,全身暖烘烘的,感到比喝咖啡还要舒服。</p><p>林林第一次感受到了喝茶的妙处。现在她才想起,父亲在她坚持只喝咖啡时,告诉并不是只有咖啡才能提神,是她的小资思想在作怪,不懂得感受中国的传统文化。现在想来,还真被父亲说中了。</p><p>“宝,来,快喝水,冲淡肚子里的茶汁,你会好受点。”王其军见林林把茶一饮而尽,急忙倒了一杯水,递给她说。</p><p>“不用了,以后我也可以喝茶了。”</p><p>“真的?”</p><p>“真的。”</p><p>“好,以后我陪你喝咖啡,你陪我喝茶。”</p><p>“好,一定。”这时,林林想起那个被她藏在沙发下的文件袋,不觉笑了,一个一喝咖啡就吐的人愿意陪你喝咖啡,他会轻易出轨吗?</p>

2026/9/20
阅读更多

第一次面试

<p>&nbsp; &nbsp; &nbsp; &nbsp; 9月6日是一个不平凡的日子,也是实破自我的日子.就在9月5日,班 主任通知要竞选班委.让大家准备自荐书。竞选的班委包括社区长(类似班长),团支书、宣传委员、心理委员、组织委员,生活委员(查寝查违禁品),社区 负责人(连接各社区长)、学生助理(跟着老师,完成老师下发的任务)</p><p>&nbsp; &nbsp; &nbsp; &nbsp; 在5日当晚9:30军训结束后,我回到宿舍准备自荐书到晚上12点。 然而在6日老师突然通知竞选班委还需要进方面试。对于我这个从未面试 过的人,内心很慌,心跳极快。这个状态一直持续到下午的面试,在4点 左右.老师通知竞选社区长和团支书的同学到13号楼面试.刚来学校没几 天.才把学校标志情建筑记住,像这类多少号楼我还没搞明自、在路上问 了4位学长、学姐才成功的进了教室。</p><p>&nbsp; &nbsp; &nbsp; &nbsp; 进了教室,我更慌了。原因有二:①第一次面试确实紧张;②我的第一意向是学生助理,但我不太擅长办公软件,没有竞争力,只能暂时搁浅、转为第二意向:记律委员,生活委员。也就是说在社区长和团支书的职位位我无心竞选。</p><p>&nbsp; &nbsp; &nbsp; &nbsp; 虽然我没有准备这两个岗位的自荐,但我肯定是有准备自我介绍啊,但我万万没想到,当我上台发言的一瞬间、大脑一片空白,什么都忘记了,只记得:“你好.我是第三社区的×××"其余的什么都没有记住。<br></p><p>&nbsp; &nbsp; &nbsp; &nbsp; 当时我呆愣在讲台上了.老师们也无语地看着我,场面一度凝固,之后老师和助理们只是简单地问了几个问题,这让我很有挫败感和不服气。光一份稿子,来回修改了三四遍,却一点也没发挥出来,我非常伤心和不服。于是我回到宿舍 ,重新写稿、准备进行下次面试其他岗位。<br></p><p>&nbsp; &nbsp; &nbsp; &nbsp; 在6日晚8:20.我与室友刚到北门拿饮水机,就看到了班主任的钉钉群信息。要求参与其他班委的同学一同过来,也就是说这是我最后竞选班干的机会。我可以接受老师找了比我优秀的同学而导致我的落选,但接受不了我连表达的机会都没有便下场了。于是我一定要去参加晚上的面试。当时我在北门, 而面试地距离14号楼有点远,我在学校东方,时间很紧,于是, 我用了20分钟从北为门跑到宿舍拿稿再跑到14号楼进行面试。可能是 我路上跑得快,整个人荷尔蒙爆发,异常自信,也可能是我内心的小宇宙爆发 强烈要求打一场翻身仗。</p><p></p><p>&nbsp; &nbsp; &nbsp; &nbsp; 当我走进教室,我异常兴奋,也有一丢丢的紧张。在9:30时轮到我上台了.<br></p><p>&nbsp; &nbsp; &nbsp; &nbsp; 我找了一个人帮我录像,记录一下我这翻身一刻。当我站在台上时有个助理 对我笑着说:又来了?我不好意思笑了笑回声是的!整个面试过程 十分顺利,面对着老师,助理和竞选的同学门并没有怯场,而是侃侃而谈,感觉老师对我的印象大为改观。之后一位助理说:如果你没有意选上这个岗位.其他的你可以接受吗?”那我必能接受啊</p><p>&nbsp; &nbsp; &nbsp; &nbsp; 在6日晚上10:30分、班主任和我打电活说:我认真地看了看你的自荐书,多对你也有一点了解了,我想可以担任资助委员,你可以的吗?<br></p><p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; “我当然可以!”<br></p><p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 之后我反思了一下,如果在下午的面试失败后没有鼓足勇气选择再 次准备去竞选.我想班委已经与我无缘了。更重要的是机会是留给有准备的 人的,如果空手去打仗,那么胜利的概率会很小,更要坚定内心信念,决 对不能放弃!自信!放松!我一定行!<br></p>

2022/10/26
阅读更多

推荐订阅

| all | digests | by\_owner | questions | tasks | | ---- | ------- | --------- | --------- | ----- | | 最新 | 精华 | 只看星主 | 问答 | 作业 |