分布式鎖實踐指南:Redis篇

目前越來越多的應用使用負載均衡,以往傳統(tǒng)單體應用單機部署的情況下使用的JAVA并發(fā)處理資源競爭方式(J.U.C或synchronized等)在集群部署中已經(jīng)無法保證資源的安全訪問。


為什么需要分布式鎖

需要考慮以下情況——

  • 只允許一個客戶端操作共享資源:這種情況下紊馏,對共享資源的操作一般是非冪等性操作银受。在這種情況下沧烈,如果出現(xiàn)多個客戶端操作共享資源履羞,就可能意味著數(shù)據(jù)不一致枣申,數(shù)據(jù)丟失嚷缭。
  • 允許多個客戶端操作共享資源:對共享資源的操作一定是冪等性操作饮亏,無論你操作多少次都不會出現(xiàn)不同結果耍贾。在這里使用鎖,無外乎就是為了避免重復操作共享資源從而提高效率路幸。

為了解決分布式應用中對資源的安全訪問于是便有了分布式鎖荐开。

實現(xiàn)分布式鎖一般基于Zookeeper或者Redis,前者可靠性高而后者效率高简肴。

如果并發(fā)量不大追求可靠性選擇Zookeeper晃听,反之選擇Redis。


Redis實現(xiàn)分布式鎖

分布式鎖一大特點就是排他性砰识,臨界條件下僅有獲得鎖的調(diào)用者才能訪問資源能扒。

而Redis是基于內(nèi)存設計K-V數(shù)據(jù)庫且單線程執(zhí)行命令。

因此使用Redis作為分布式鎖的中間件滿足兩個重要條件——

  • 基于內(nèi)部:加鎖/解鎖效率高
  • 單線程:請求先后順序執(zhí)行沒有并發(fā)沖突辫狼,所以任意時刻只有一個調(diào)用者能成功獲取鎖初斑。

而且 Redis支持集群部署(sentinel, cluster)保障了可用性。


加鎖

使用SETNX()便可以完成加鎖操作膨处。SETNX表示如果Key不存在則寫入见秤,如果存在則什么都不做。

setnx key value

這樣便只允許一個調(diào)用者可以訪問真椿。等等似乎少了什么鹃答?沒錯我們沒有設置KEY的過期時間,如果此時調(diào)用者宕機這把鎖就無法釋放了突硝。

我們使用EXPIRE加上過期時間测摔,默認單位為秒。

EXPIRE key seconds

但是這樣做就完了嗎解恰?SETNXEXPIRE是兩個獨立的操作锋八,即加鎖操作不具備原子性。假如加鎖調(diào)用成功修噪,但是設置過期時間的時候調(diào)用服務宕機查库,依然存在鎖無法正常釋放的問題路媚。

于是我們還需要把這兩條命令合為一條命令黄琼。

SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
  • expiration:過期時間,EX表示秒整慎,PX表示毫秒
  • NX:表示如果不存在則寫入(顯然我們需要這條語句)
  • XX:表示如果存在則寫入

目前終于算是完成了原子加鎖命令并且鎖存在過期時間即使調(diào)用者宕機到期之后也會自動釋放脏款。


解鎖

使用DEL便可以刪除鎖。

DEL key [key ...]

不過事情真的有這么簡單嗎裤园?當然不會(套路啊)

  • 假設調(diào)用者A加鎖成功并且設置鎖超時時間為30秒撤师。但是因為某些原因導致調(diào)用者A處理業(yè)務邏輯所花費的時間超過了30秒。
  • 鎖超時自動釋放之后調(diào)用者B剛好加鎖成功拧揽,這時調(diào)用者A使用DEL語句釋放了調(diào)用者B的鎖剃盾。
  • 因為鎖被調(diào)用者A釋放導致后續(xù)的調(diào)用者可以參與鎖競爭腺占,例如調(diào)用者C獲取到鎖。
  • 從而發(fā)送在同一個時間內(nèi)調(diào)用者B與調(diào)用者C同時運行業(yè)務邏輯從而破壞了資源的安全訪問痒谴。

別繞暈了畫張圖更直觀的感受下——

從圖中可以發(fā)現(xiàn)A衰伯、B有并行,B积蔚、C有并行不符合我們的要求意鲸。

那么怎樣才能讓調(diào)用者只能釋放自己占用的鎖呢?這個時候K-V中的V就發(fā)揮作用了尽爆。如果我們的Value都是唯一(例如:UUID)的并且調(diào)用者釋放鎖需要驗證Value便可以避免釋放其他調(diào)用者占用的鎖資源怎顾。

SET key uuid EX times NX

那么在釋放鎖的時候就跟需要分三步——

  1. 根據(jù)KEY獲取鎖
  2. 對比VALUE與預期值是否一致
  3. 如果一致則釋放(刪除)鎖

可是這三個步驟是獨立非原子操作,如果在2漱贱、3步驟之間鎖因為超時自動釋放且在高并發(fā)情況下別其他調(diào)用者加鎖成功槐雾,在執(zhí)行步驟3時則仍然存在刪除其他調(diào)用者的鎖。

因此我們?nèi)匀恍枰硬僮鞣ā_z憾的是Redis的命令并不提供該操作蚜退。

別慌Redis雖然沒有提供,但是Redis支持LUA腳本彪笼,LUA腳本可以幫助原子性的完成這一系列復合操作钻注。

以下為Redis官方提供腳本——

if redis.call('get', KEYS[1]) == ARGV[1] 
then 
  return redis.call('del', KEYS[1]) 
else 
  return 0 
end

這個腳本即使你不會LUA大概也猜得到是什么意思吧。

  1. 傳入的鍵名參數(shù)調(diào)用get操作獲取到的值與附加參數(shù)比較
  2. 如果值一致則調(diào)用del操作(刪除操作成功返回1)
  3. 如果值不一致則返回0

執(zhí)行腳本如下——

eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock value

現(xiàn)在釋放鎖的原子操作也滿足了配猫,讓我們看看代碼實踐幅恋。


代碼示例

筆者這里使用springboot來演示分布式鎖。

因為分布式鎖的實現(xiàn)可以是多種方式建議抽象一個接口泵肄,例如DistributedLock捆交。

如果使用Redis作為分布式鎖中間件則創(chuàng)建RedisDistributedLock

如果使用Zookeeper作為分布式鎖的中間件則創(chuàng)建ZookeeperDistributedLock腐巢。

可以根據(jù)@ConditionalOnBean以及spring優(yōu)先級選擇加載那種分布式鎖品追。

  • 分布式鎖配置

    spring:
      redis:
        lettuce:
          pool:
            max-active: 100
            max-idle: 50
            min-idle: 30
            max-wait: 2000ms
        # 哨兵模式
        sentinel:
          master: mymaster
          nodes: ${ARGS_REDIS_NODES}
        # 集群模式
        # cluster:
          # nodes: ${ARGS_REDIS_NODES}
        password: ${ARGS_REDIS_PASSWORD}
    
    
    # 分布式配置
    distributed:
      lock:
        expire: 30000
        # connection-timeout-ms: 15000
        # session-timeout-ms: 60000
    
  • 分布式鎖接口

    public interface DistributedLock {
    
        /**
         * 獲取鎖
         *
         * @param key
         * @param value
         * @return
         */
        public boolean tryLock(String key, String value);
    
        /**
         * 釋放鎖
         *
         * @param key
         * @param value
         * @return
         */
        public boolean releaseLock(String key, String value);
    
    }
    
  • 分布式鎖實現(xiàn)

    /**
     * 分布式鎖:使用Redis實現(xiàn)
     */
    @AutoConfigureAfter({RedisAutoConfiguration.class})
    @ConditionalOnBean(RedisAutoConfiguration.class)
    @Service
    @Slf4j
    public class RedisDistributedLock implements DistributedLock {
    
        private final static String OK = "OK";
    
        private static final String UNLOCK_LUA;
    
        /**
         * Lua腳本來釋放鎖
         */
        static {
            StringBuilder sb = new StringBuilder();
            sb.append("if redis.call('get', KEYS[1]) == ARGV[1] ");
            sb.append("then ");
            sb.append("    return redis.call('del', KEYS[1]) ");
            sb.append("else ");
            sb.append("    return 0 ");
            sb.append("end");
            UNLOCK_LUA = sb.toString();
        }
    
        /**
         * 缺省超時時間
         */
        @Value("${distributed.lock.expire}")
        private long expire;
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean tryLock(String key, String value) {
            return tryLock(key, value, TimeUnit.MILLISECONDS, expire);
        }
    
        @Override
        public boolean releaseLock(String key, String value) {
            try {
                return stringRedisTemplate.execute(
                  (RedisCallback<Boolean>) connection -> connection.eval(
                        UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1,
                        key.getBytes(Charset.forName("UTF-8")),
                        value.getBytes(Charset.forName("UTF-8"))));
            } catch (Exception ex) {
                log.error("釋放redis鎖失敗", ex);
            }
            return false;
    
        }
    
        /**
         * 獲取鎖
         *
         * @param key
         * @param value
         * @param timeUnit
         * @param time
         * @return
         */
        private boolean tryLock(String key, String value, 
                                TimeUnit timeUnit, long time) {
            try {
                return stringRedisTemplate.execute(
                  (RedisCallback<Boolean>) connection -> connection.set(
                        key.getBytes(Charset.forName("UTF-8")),
                        value.getBytes(Charset.forName("UTF-8")),
                        Expiration.milliseconds(timeUnit.toMillis(time)),
                        RedisStringCommands.SetOption.SET_IF_ABSENT)
                );
            } catch (Exception ex) {
                log.error("獲取redis鎖失敗", ex);
            }
            return false;
        }
    
    }
    
  • 調(diào)用示例

    boolean lock = distributedLock.tryLock(LOCK_NAME, lockValue);
    try {
      if (!lock) {
        log.info("已存在分布式鎖:[{}]", LOCK_NAME);
        return;
      }
    } finally {
      if (lock) {
        distributedLock.releaseLock(LOCK_NAME, lockValue);
      }
    }
    

以上就是對分布式鎖的代碼演示。

分布式鎖的疑問

Redis實現(xiàn)分布式鎖還有哪些問題呢冯丙?

  • 自動續(xù)期

    當業(yè)務邏輯處理時間超過鎖的過期時間需要有監(jiān)控線程在過期之前進續(xù)期肉瓦。你可以將其稱之為Monitor或者Watchdog

  • 可重入

    分布式鎖能夠支持一個線程對資源的重復加鎖嗎胃惜?

  • 讀寫鎖

    所謂讀寫鎖即:讀讀不互斥泞莉,讀寫互斥,寫寫互斥船殉。例如:ReentrantReadWriteLock鲫趁。能否想使用J.U.C包下面的類一樣使用分布式鎖呢?

  • 集群

    Redis集群環(huán)境是一個復雜的邏輯結構利虫,節(jié)點的上線下線挨厚、主從復制堡僻、從節(jié)點的提升導致的數(shù)據(jù)丟失考慮了嗎?

是不是感覺頭都大了疫剃。

好在Redis官方給我們指了一條明路苦始。這就是接下來筆者要介紹的內(nèi)容了。


Redisson介紹

Redisson正式筆者要介紹的Redis分布式鎖終極解決方案慌申。什么你沒聽說過陌选?沒關系跟著筆者進入新世界的大門。


什么是Redisson?

Redisson是一個在Redis的基礎上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)蹄溉。它不僅提供了一系列的分布式的Java常用對象咨油,還提供了許多分布式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法柒爵。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern)役电,從而讓使用者能夠將精力更集中地放在處理業(yè)務邏輯上。

關于Redisson項目的詳細介紹可以在官方網(wǎng)站找到棉胀。每個Redis服務實例都能管理多達1TB的內(nèi)存法瑟。

能夠完美的在云計算環(huán)境里使用,并且支持AWS ElastiCache主備版唁奢,AWS ElastiCache集群版霎挟,Azure Redis Cache阿里云(Aliyun)的云數(shù)據(jù)庫Redis版

一言以蔽之可以像使用本地J.U.C一樣使用Redis分布式鎖。


Redisson示例

  • 新增依賴支持

    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.13.2</version>
    </dependency>
    
  • YAML文件配置

    redisson:
      client:
        sentinel-address:
          - redis://${redis.senntinel.node1}
          - redis://${redis.senntinel.node2}
          - redis://${redis.senntinel.node3}
        master-name: mymaster
        database: 0
        password: ${redis.password}
    
  • 新增RedissonConfig配置類

    @Configuration
    @ConfigurationProperties(prefix = "redisson.client")
    @Getter
    @Setter
    public class RedissonConfig {
      
      private String masterName;
      
      private int database;
      
      private String password;
      
      private String[] sentinelAddress;
      
      @Bean(destroyMethod = "shutdown")
      public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSentinelServers()
          .setMasterName(masterName)
          .setPassword(password)
          .setDatabase(database)
          .setCheckSentinelsList(false)
          .addSentinelAddress(sentinelAddress);
        return Redisson.create(config);
      }
    
    }
    
  • 測試方法

    @GetMapping(value = "/api/lock")
    public String testRedisson() throws InterruptedException {
      // 鎖對象
      RLock rLock = redissonClient.getLock("lock");
      // rLock.tryLock();
      // 阻塞式獲取鎖
      rLock.lock();
      try {
        log.info("the lock for {}-{}", Thread.currentThread().getName(), Thread.currentThread().getId());
        // 超過默認的30秒鎖過期時間
        Thread.sleep(1000 * 120);
      } finally {
        rLock.unlock();
        log.info("release the lock for {}-{}", Thread.currentThread().getName(), 
                 Thread.currentThread().getId());
      }
      return "";
    }
    

    驗證結果如下所示——

    EDB3D857-1A1C-4A6C-9915-C350705E40A2.jpg

    由此可以Redisson幫我們自動續(xù)期麻掸。抓取一下Redis的請求信息看看——

    "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 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; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97"
    
    "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 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; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
    


Redisson代碼

觀察代碼發(fā)現(xiàn)使用LUA腳本續(xù)期酥夭,Redisson中大量使用LUA腳本保證其命令具有原子性。

1594739476.909029 [0 101.88.95.75:25242] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 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; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97"
1594739476.909135 [0 lua] "exists" "lock"
1594739476.909158 [0 lua] "hincrby" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:97" "1"
1594739476.909176 [0 lua] "pexpire" "lock" "30000"
1594739476.909201 [0 101.88.95.75:25242] "WAIT" "2" "1000"
1594739477.166176 [0 10.10.10.101:36452] "PING"
1594739477.452893 [0 10.10.10.101:36468] "PING"
1594739477.619219 [0 10.10.10.101:36468] "PUBLISH" "__sentinel__:hello" "172.17.0.3,26380,2d8a2463c5242b26b0fc58a07b444ed28fca45c6,0,mymaster,10.10.10.101,6379,0"
1594739477.766886 [0 10.10.10.101:36484] "PING"
1594739477.870853 [0 10.10.10.101:36484] "PUBLISH" "__sentinel__:hello" "172.17.0.3,26381,037ccf65d8178a1aaac14bbb40fc28b2cebd4eac,0,mymaster,10.10.10.101,6379,0"
1594739478.017627 [0 101.88.95.75:25247] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 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; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.017727 [0 lua] "exists" "lock"
1594739478.017738 [0 lua] "hexists" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.017747 [0 lua] "pttl" "lock"
1594739478.017758 [0 101.88.95.75:25247] "WAIT" "2" "1000"
1594739478.047182 [0 101.88.95.75:25263] "SUBSCRIBE" "redisson_lock__channel:{lock}"
1594739478.065068 [0 101.88.95.75:25251] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 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; return redis.call('pttl', KEYS[1]);" "1" "lock" "30000" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.065226 [0 lua] "exists" "lock"
1594739478.065241 [0 lua] "hexists" "lock" "edabe2d0-b3d5-4526-b69a-952f1fc994cc:91"
1594739478.065256 [0 lua] "pttl" "lock"
1594739478.065270 [0 101.88.95.75:25251] "WAIT" "2" "1000"

MONITOR日志中也證實了Redisson操作使用了大量的LUA腳本脊奋。

Redisson的鎖集成了J.U.C下的Lock接口熬北,提供分布式的重入鎖讀寫鎖等等诚隙,讀者完全可以放心大膽的使用Redisson實現(xiàn)分布式鎖讶隐。

Redisson的時候場景非常多,由于篇幅現(xiàn)在這里筆者就點到為止久又。建議去Redisson官網(wǎng)去進一步學習巫延。


尾聲

一個合格的Redis分布式鎖考慮到多少問題。在實際使用中還需要考慮到KEY的設計以及鎖的粒度問題籽孙。粒度越低影響越少烈评,只鎖定訪問共享數(shù)據(jù)的代碼盡可能的降低鎖的粒度火俄。

Redis分布式鎖就介紹到這里犯建,看到這里的你是否有所收獲呢?

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓜客,一起剝皮案震驚了整個濱河市适瓦,隨后出現(xiàn)的幾起案子竿开,更是在濱河造成了極大的恐慌,老刑警劉巖玻熙,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件否彩,死亡現(xiàn)場離奇詭異,居然都是意外死亡嗦随,警方通過查閱死者的電腦和手機列荔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枚尼,“玉大人贴浙,你說我怎么就攤上這事∈鸹校” “怎么了崎溃?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盯质。 經(jīng)常有香客問我袁串,道長,這世上最難降的妖魔是什么呼巷? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任囱修,我火速辦了婚禮,結果婚禮上王悍,老公的妹妹穿的比我還像新娘蔚袍。我一直安慰自己,他們只是感情好配名,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布啤咽。 她就那樣靜靜地躺著,像睡著了一般渠脉。 火紅的嫁衣襯著肌膚如雪宇整。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天芋膘,我揣著相機與錄音鳞青,去河邊找鬼。 笑死为朋,一個胖子當著我的面吹牛臂拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播习寸,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼胶惰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霞溪?” 一聲冷哼從身側響起孵滞,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤中捆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后坊饶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泄伪,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年匿级,在試婚紗的時候發(fā)現(xiàn)自己被綠了蟋滴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡痘绎,死狀恐怖脓杉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情简逮,我是刑警寧澤球散,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站散庶,受9級特大地震影響蕉堰,放射性物質發(fā)生泄漏。R本人自食惡果不足惜悲龟,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一屋讶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧须教,春花似錦皿渗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贬养,卻和暖如春挤土,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背误算。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工仰美, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人儿礼。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓咖杂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蚊夫。 傳聞我的和親對象是個殘疾皇子诉字,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 來自公眾號:非科班的科班作者:黎杜 前言 標題使用最近異城火熱的微信拍一拍的方式命名埃唯,最近拍一拍的玩法被各位網(wǎng)友玩...
    碼農(nóng)小光閱讀 1,135評論 0 22
  • 來自公眾號:非科班的科班作者:黎杜 前言 標題使用最近異衬@火熱的微信拍一拍的方式命名漠趁,最近拍一拍的玩法被各位網(wǎng)友玩...
    夜空_2cd3閱讀 384評論 0 4
  • 分布式鎖 什么是鎖?使用鎖的目的是為了控制程序的執(zhí)行順序忍疾,防止共享資源被多個線程同時訪問闯传。為了實現(xiàn)多個線程在一個時...
    緣起緣散_f1a7閱讀 219評論 0 0
  • 久違的晴天,家長會卤妒。 家長大會開好到教室時甥绿,離放學已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗则披。 放學鈴聲...
    飄雪兒5閱讀 7,494評論 16 22
  • 創(chuàng)業(yè)是很多人的夢想共缕,多少人為了理想和不甘選擇了創(chuàng)業(yè)來實現(xiàn)自我價值,我就是其中一個士复。 創(chuàng)業(yè)后图谷,我由女人變成了超人,什...
    亦寶寶閱讀 1,802評論 4 1