Administrator
发布于 2023-04-18 / 0 阅读
0
0

redis分布式锁

redis分布式锁

ᅩᩩٚ፡ِ҅౮ԟబ҅ஙמ൤ᔱ̓ӣॡৼයӰ̈́ىဳᬯӻ԰ᘶᗑᝌӬ؎ኞጱૡٍՈ̶

๜෈ GitHub https://github.com/JavaFamily ૪ත୯҅ํӞᕚय़ܯᶎᦶਠෆᘍᅩ̵ᩒාզ݊౯

ጱᔮڜ෈ᒍ̶

ڹ᥺


ӤӞᒍᜓ౯൉کԧचԭzkړ૲ୗᲁጱਫሿ҅ᬯᒍᜓ੪๶᧔ӞӥचԭRedisጱړ૲ୗᲁਫሿމ̶

zkਫሿړ૲ୗᲁጱփᭆᳪғzkړ૲ୗᲁ

ࣁ୏ত൉کRedisړ૲ୗᲁԏڹ҅౯మ᪙य़ਹᘱᅩRedisጱचᏐᎣᦩ̶

᧔ӞӥRedisጱӷӻ޸եғ

setnx ฎSET if Not eXists(ইຎӧਂࣁ҅ڞ SET)ጱᓌ̶ٟ

አဩইࢶ҅ইຎӧਂࣁset౮ۑᬬࢧintጱ1҅ᬯӻkeyਂࣁԧᬬࢧ0̶

ਖ਼꧊ value ىᘶک key ҅ଚਖ਼ key ጱኞਂ෸ᳵᦡԅ seconds (զᑁԅܔ֖)̶

ইຎ key ૪ᕪਂࣁ҅ setex ޸եਖ਼ᥟٟ෯꧊̶

ํੜվ֎ᙗਧտወజӡӞset value ౮ۑ set time०ᨳ҅ᮎӧ੪؞ԧԍ҅ᬯࠡRedisਥᗑమکԧ̶

setex ฎӞӻܻৼ௔(atomic)඙֢҅ىᘶ꧊޾ᦡᗝኞਂ෸ᳵӷӻ֢ۖտࣁݶӞ෸ᳵٖਠ౮̶

SETNX key value

SETEX key seconds value

౯ᦡᗝԧ10ᑁጱ०ප෸ᳵ҅ttl޸եݢզັ፡ׯᦇ෸҅ᨮጱ᧔ก૪ᕪک๗ԧ̶

᪙य़ਹᦖᬯӷӻ޸ݷԞฎํܻࢩጱ҅ࢩԅ՜ժฎRedisਫሿړ૲ୗᲁጱىᲫ̶

ྋ෈


୏তڹᬮฎ፡፡࣋ว:

౯ׁᆐฎڠୌԧஉग़ӻᕚᑕ݄ಕٺପਂinventory҅ӧڊ఺क़ጱପਂಕٺᶲଧݒԧ๋҅ᕣጱᕮຎԞฎӧ੒

ጱ̶

ܔ๢ےsynchronized౲ᘏLockᬯԶଉᥢ඙֢౯੪ӧ᧔ԧঅމ҅ᕮຎᙗਧฎ੒ጱ̶

౯ضਫሿӞӻᓌܔጱRedisᲁ҅ᆐݸ౯ժٚਫሿړ૲ୗᲁ҅ݢᚆๅො׎य़ਹጱቘᥴ̶

ᬮᦕ஑Ӥᶎ౯᧔ᬦጱ޸եԍ҅ਫሿӞӻܔ๢ጱٌਫྲ᫾ᓌܔ֦҅ժض௏ᘍӞӥ҅ڦஃӥ፡̶

setnx


ݢզ፡ک҅ᒫӞӻ౮ۑԧ҅ဌ᯽නᲁ҅ݸᶎጱ᮷०ᨳԧ҅ᛗ੝ᶲଧᳯ᷌ᳯ᷌ฎᥴ٬ԧ҅ݝᥝےᲁ҅ᖽන

ݸᶎጱ೭ک҅᯽නইྌ஗ሾ҅੪ᚆכᦤೲᆙᶲଧಗᤈ̶

֕ฎ֦ժԞݎሿᳯ᷌ԧ҅ᬮฎӞ໏ጱ҅ᒫӞӻ՚set౮ۑԧ҅֕ฎᑱᆐ೯ԧ҅ᮎᲁ੪Ӟፗࣁᮎ෫ဩ஑ک᯽

න҅ݸᶎጱᕚᑕԞ࿞ᬱ஑ӧکᲁ݈҅ྒᲁԧ̶

ಅզ....

setex


Ꭳ᭲౯ԏڹ᧔ᬯӻ޸եጱܻࢩԧމ҅ᦡᗝӞӻᬦ๗෸ᳵ҅੪ᓒᕚᑕ1೯ԧ҅Ԟտࣁ०ප෸ᳵکԧ҅ᛔۖ

᯽න̶

౯ᬯ᯾੪አکԧnx޾pxጱᕮݳ݇හ҅੪ฎset꧊ଚӬےԧᬦ๗෸ᳵ҅ᬯ᯾౯ᬮᦡᗝԧӞӻᬦ๗෸ᳵ҅੪

ฎᬯ෸ᳵٖইຎᒫԫӻဌ೭کᒫӞӻጱᲁ҅੪ᭅڊᴥलԧ҅ࢩԅݢᚆฎਮಁᒒෙᬳԧ̶

ےᲁ


ෆ֛ےᲁጱ᭦ᬋྲ᫾ᓌܔ҅य़ਹच๜Ӥ᮷ᚆ፡౜҅ӧᬦ౯೭ک୮ڹ෸ᳵ݄ٺ୏ত෸ᳵጱ඙֢ఽᥧํᅩ

ᒨ҅ System.currentTimeMillis()ၾᘙஉय़ጱ̶

/**

  • ےᲁ
  • *

  • @param id
  • @return
  • */

    public boolean lock(String id) {

    Long start = System.currentTimeMillis();

    try {

    for (; ; ) {

    //SET޸եᬬࢧOK ҅ڞᦤก឴ݐᲁ౮ۑ

    String lock = jedis.set(LOCK_KEY, id, params);

    if ("OK".equals(lock)) {

    return true;

    }

    //ވڞ஗ሾᒵஇ҅ࣁtimeout෸ᳵٖՖ๚឴ݐکᲁ҅ڞ឴ݐ०ᨳ

    long l = System.currentTimeMillis() - start;

    System.currentTimeMillisၾᘙय़҅ྯӻᕚᑕᬰ๶᮷ᬯ໏҅౯ԏڹٟդᎱ҅੪տࣁ๐ۓ࢏ސۖጱ෸ײ҅୏

    Ӟӻᕚᑕӧෙ݄೭҅᧣አොፗള឴ݐ꧊੪অԧ҅ӧᬦԞӧฎ๋սᥴ҅෭๗ᔄᬮฎํஉग़অොဩጱ̶

    if (l >= timeout) {

    return false;

    }

    try {

    Thread.sleep(100);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    }

    } finally {

    jedis.close();

    }

    }

    @Service

    public class TimeServcie {

    private static long time;

    static {

    new Thread(new Runnable(){

    @Override

    public void run() {

    while (true){

    try {

    Thread.sleep(5);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    long cur = System.currentTimeMillis();

    setTime(cur);

    }

    }

    }).start();

    }


    public static long getTime() {

    return time;

    }


    public static void setTime(long time) {

    TimeServcie.time = time;

    }


    ᥴᲁ


    ᥴᲁጱ᭦ᬋๅےᓌܔ҅੪ฎӞྦྷLuaጱ೪ᤰ҅಩Key؉ԧڢᴻ̶

    ֦ժݎሿဌ҅౯Ӥᶎےᲁᥴᲁ᮷አԧUUID҅ᬯ੪ฎԅԧכᦤ҅᧡ےᲁԧ᧡ᥴᲁ҅ᥝฎ֦ڢധԧ౯ጱ

    ᲁ҅ᮎӧԤॺԧࡶ̶

    LUAฎܻৼ௔ጱ҅Ԟྲ᫾ᓌܔ҅੪ฎڣෙӞӥKey޾౯ժ݇හฎވፘᒵ҅ฎጱᦾ੪ڢᴻ҅ᬬࢧ౮ۑ1҅0

    ੪ฎ०ᨳ̶

    ḵᦤ


    ౯ժݢզአ౯ժٟጱRedisᲁᦶᦶපຎ҅ݢզ፡ک᮷ೲᆙᶲଧ݄ಗᤈԧ

    }

    /**

  • ᥴᲁ
  • *

  • @param id
  • @return
  • */

    public boolean unlock(String id) {

    String script =

    "if redis.call('get',KEYS[1]) == ARGV[1] then" +

    " return redis.call('del',KEYS[1]) " +

    "else" +

    " return 0 " +

    "end";

    try {

    String result = jedis.eval(script,

    Collections.singletonList(LOCK_KEY),

    Collections.singletonList(id)).toString();

    return "1".equals(result) ? true : false;

    } finally {

    jedis.close();

    }

    }

    ௏ᘍ


    य़ਹฎӧฎᥧ஑ਠᗦԧ҅֕ฎӤᶎጱᲁ҅ํӧ੝ቭዟጱ҅౯ဌ௏ᘍஉग़ᅩ֦҅౲ᦜݢզ௏ᘍӞӥ҅რᎱ

    ౯᮷୏რک౯ጱGItHubԧ̶

    ᘒӬ҅ᲁӞᛱ᮷ฎᵱᥝݢ᯿فᤈጱ҅Ӥᶎጱᕚᑕ᮷ฎಗᤈਠԧ੪᯽නԧ҅෫ဩེٚᬰفԧ҅ᬰ݄Ԟฎ᯿

    ෛےᲁԧ҅੒ԭӞӻᲁጱᦡᦇ๶᧔ᙗਧӧฎஉݳቘጱ̶

    ౯ӧ಑ᓒಋٟ҅ࢩԅ᮷ํሿ౮ጱ҅ڦՈଆ౯ժٟঅԧ̶

    redisson


    redissonጱᲁ҅੪ਫሿԧݢ᯿فԧ҅֕ฎ՜ጱრᎱྲ᫾ฤ႑ᵙ౜̶

    ֵአ᩸๶உᓌܔ҅ࢩԅ՜ժବ੶᮷੗ᤰঅԧ֦҅ᬳളӤ֦ጱRedisਮಁᒒ҅՜ଆ֦؉ԧ౯ӤᶎٟጱӞ

    ڔ҅ᆐݸๅਠᗦ̶

    ᓌܔ፡፡՜ጱֵአމ҅᪙ྋଉֵአLockဌࠨ܄ڦ̶

    ThreadPoolExecutor threadPoolExecutor =

    new ThreadPoolExecutor(inventory, inventory, 10L, SECONDS,

    linkedBlockingQueue);

    long start = System.currentTimeMillis();

    Config config = new Config();

    config.useSingleServer().setAddress("redis://127.0.0.1:6379");

    final RedissonClient client = Redisson.create(config);

    Ӥᶎݢզ፡ک౯አکԧgetLockٌ҅ਫ੪ฎ឴ݐӞӻᲁጱਫ̶ֺ

    RedissionLock Ԟဌ؉ࠨ҅੪ฎᆧఀጱڡত۸̶

    ےᲁ


    ํဌํݎሿஉग़᪙Lockஉग़ፘ֒ጱࣈොޫҘ

    ੤ᦶےᲁ҅೭ک୮ڹᕚᑕ҅ᆐݸ౯୏१᧔ጱttlԞ፡کԧ҅ฎӧฎӞڔ᮷ฎᮎԍᆧఀҘ

    final RLock lock = client.getLock("lock1");


    for (int i = 0; i <= NUM; i++) {

    threadPoolExecutor.execute(new Runnable() {

    public void run() {

    lock.lock();

    inventory--;

    System.out.println(inventory);

    lock.unlock();

    }

    });

    }

    long end = System.currentTimeMillis();

    System.out.println("ಗᤈᕚᑕහ:" + NUM + " ௛ᘙ෸:" + (end - start) + " ପ

    ਂහԅ:" + inventory);

    public RLock getLock(String name) {

    return new RedissonLock(connectionManager.getCommandExecutor(), name);

    }


    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {

    super(commandExecutor, name);

    //޸եಗᤈ࢏

    this.commandExecutor = commandExecutor;

    //UUIDਁᒧԀ

    this.id = commandExecutor.getConnectionManager().getId();

    //ٖ᮱ᲁᬦ๗෸ᳵ

    this.internalLockLeaseTime = commandExecutor.

    getConnectionManager().getCfg().getLockWatchdogTimeout();

    this.entryName = id + ":" + name;

    }

    ឴ݐᲁ


    ឴ݐᲁጱ෸ײ҅Ԟྲ᫾ᓌܔ֦҅ݢզ፡ک҅՜Ԟฎӧෙڬෛᬦ๗෸ᳵ҅᪙౯Ӥᶎӧෙ݄೭୮ڹ෸ᳵ໊҅

    ḵᬦ๗ฎӞӻ᭲ቘ҅ݝฎ౯ྲ᫾ᔋᔧ̶

    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws

    InterruptedException {


    //୮ڹᕚᑕID

    long threadId = Thread.currentThread().getId();

    //੤ᦶ឴ݐᲁ

    Long ttl = tryAcquire(leaseTime, unit, threadId);

    // ইຎttlԅᑮ҅ڞᦤก឴ݐᲁ౮ۑ

    if (ttl == null) {

    return;

    }

    //ইຎ឴ݐᲁ०ᨳ҅ڞᦈᴅک੒ଫᬯӻᲁጱchannel

    RFuture future = subscribe(threadId);

    commandExecutor.syncSubscription(future);


    try {

    while (true) {

    //ེٚ੤ᦶ឴ݐᲁ

    ttl = tryAcquire(leaseTime, unit, threadId);

    //ttlԅᑮ҅᧔ก౮ۑ឴ݐᲁ҅ᬬࢧ

    if (ttl == null) {

    break;

    }

    //ttlय़ԭ0 ڞᒵஇttl෸ᳵݸᖀᖅ੤ᦶ឴ݐ

    if (ttl >= 0) {

    getEntry(threadId).getLatch().tryAcquire(ttl,

    TimeUnit.MILLISECONDS);

    } else {

    getEntry(threadId).getLatch().acquire();

    }

    }

    } finally {

    //ݐၾ੒channelጱᦈᴅ

    unsubscribe(future, threadId);

    }

    //get(lockAsync(leaseTime, unit));

    }

    ବ੶ےᲁ᭦ᬋ


    ֦ݢᚆտమᬯԍग़඙֢҅ࣁӞ᩸ӧฎܻৼ௔ӧᬮฎํᳯ᷌ԍҘ

    य़֬ժᙗਧమ஑کޚ҅ಅզᬮฎLUA҅՜ֵአԧHashጱහഝᕮ຅̶

    Ԇᥝฎڣෙᲁฎވਂࣁ҅ਂࣁ੪ᦡᗝᬦ๗෸ᳵ҅ইຎᲁ૪ᕪਂࣁԧ҅ᮎ੒ྲӞӥᕚᑕ҅ᕚᑕฎӞӻᮎ੪

    ᦤกݢզ᯿ف҅ᲁࣁԧ҅֕ฎӧฎ୮ڹᕚᑕ҅ᦤกڦՈᬮဌ᯽න҅ᮎ੪಩ۃ֟෸ᳵᬬࢧ҅ےᲁ०ᨳ̶

    ฎӧฎํᅩᕰ҅ग़ቘᥴӞ̶᭭

    private RFuture tryAcquireAsync(long leaseTime, TimeUnit unit,

    final long threadId) {


    //ইຎଃํᬦ๗෸ᳵ҅ڞೲᆙฦ᭗ොୗ឴ݐᲁ

    if (leaseTime != -1) {

    return tryLockInnerAsync(leaseTime, unit, threadId,

    RedisCommands.EVAL_LONG);

    }


    //ضೲᆙ30ᑁጱᬦ๗෸ᳵ๶ಗᤈ឴ݐᲁጱොဩ

    RFuture ttlRemainingFuture = tryLockInnerAsync(


    commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),

    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);


    //ইຎᬮ೮ํᬯӻᲁ҅ڞ୏ސਧ෸ձۓӧෙڬෛᧆᲁጱᬦ๗෸ᳵ

    ttlRemainingFuture.addListener(new FutureListener() {

    @Override

    public void operationComplete(Future future) throws Exception

    {

    if (!future.isSuccess()) {

    return;

    }


    Long ttlRemaining = future.getNow();

    // lock acquired

    if (ttlRemaining == null) {

    scheduleExpirationRenewal(threadId);

    }

    }

    });

    return ttlRemainingFuture;

    }

    ᥴᲁ


    ᲁጱ᯽නԆᥝฎpublish᯽නᲁጱמ௳҅ᆐݸ؉໊ḵ҅Ӟ໏տڣෙฎވ୮ڹᕚᑕ҅౮ۑ੪᯽නᲁ҅ᬮํ

    ӻhincrby᭓ٺጱ඙֢҅ᲁጱ꧊य़ԭ0᧔กฎݢ᯿فᲁ҅ᮎ੪ڬෛᬦ๗෸ᳵ̶

    ইຎ꧊ੜԭ0ԧ҅ᮎڢധKey᯽නᲁ̶

    ฎӧฎ݈޾AQSஉ؟ԧҘ

    AQS੪ฎ᭗ᬦӞӻvolatileץ᷶status݄፡ᲁጱᇫா҅Ԟտ፡හ꧊ڣෙฎވฎݢ᯿فጱ̶

    ಅզ౯᧔դᎱጱᦡᦇ๋҅ݸ੪ӡڻ୭Ӟ҅᮷ฎӞ໏ጱ̶

    RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit,

    long threadId, RedisStrictCommand command) {


    //ᬦ๗෸ᳵ

    internalLockLeaseTime = unit.toMillis(leaseTime);


    return commandExecutor.evalWriteAsync(getName(),

    LongCodec.INSTANCE, command,

    //ইຎᲁӧਂࣁ҅ڞ᭗ᬦhsetᦡᗝਙጱ꧊҅ଚᦡᗝᬦ๗෸ᳵ

    "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; " +

    //ইຎᲁ૪ਂࣁ҅ଚӬᲁጱฎ୮ڹᕚᑕ҅ڞ᭗ᬦhincrbyᕳහ꧊᭓ी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; " +

    //ইຎᲁ૪ਂࣁ҅֕ଚᶋ๜ᕚᑕ҅ڞᬬࢧᬦ๗෸ᳵttl

    "return redis.call('pttl', KEYS[1]);",

    Collections.singletonList(getName()),

    internalLockLeaseTime, getLockName(threadId));

    }

    public RFuture unlockAsync(final long threadId) {

    final RPromise result = new RedissonPromise();


    //ᥴᲁොဩ

    RFuture future = unlockInnerAsync(threadId);


    future.addListener(new FutureListener() {

    @Override

    public void operationComplete(Future future) throws

    Exception {

    if (!future.isSuccess()) {

    cancelExpirationRenewal(threadId);

    result.tryFailure(future.cause());

    return;

    }

    //឴ݐᬬࢧ꧊

    Boolean opStatus = future.getNow();

    //ইຎᬬࢧᑮ҅ڞᦤกᥴᲁጱᕚᑕ޾୮ڹᲁӧฎݶӞӻᕚᑕ҅ಲڊ୑ଉ

    if (opStatus == null) {

    IllegalMonitorStateException cause =

    new IllegalMonitorStateException("

    attempt to unlock lock, not locked by current

    thread by node id: "

    + id + " thread-id: " + threadId);

    result.tryFailure(cause);

    return;

    }

    //ᥴᲁ౮ۑ҅ݐၾڬෛᬦ๗෸ᳵጱᮎӻਧ෸ձۓ

    if (opStatus) {

    cancelExpirationRenewal(null);

    }

    result.trySuccess(null);

    }

    });


    return result;

    }



    protected RFuture unlockInnerAsync(long threadId) {

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE,

    EVAL,


    //ইຎᲁ૪ᕪӧਂࣁ҅ ݎ૲ᲁ᯽නጱၾ௳

    "if (redis.call('exists', KEYS[1]) == 0) then " +

    "redis.call('publish', KEYS[2], ARGV[1]); " +

    "return 1; " +

    "end;" +

    //ইຎ᯽නᲁጱᕚᑕ޾૪ਂࣁᲁጱᕚᑕӧฎݶӞӻᕚᑕ҅ᬬࢧnull

    ௛ᕮ


    ᬯӻٟԧྲ᫾ԋ҅֕ฎӧฎࢩԅ॔๥Ջԍጱ҅ฎࢩԅӻՈૡ֢ጱܻࢩ๋҅ᬪԪఘஉग़ࡶ҅ᬮฎᮎݙᦾ҅

    ᑕଧާ಍ฎ౯ጱ๜ᘳٟ෈ᒍݝฎӻᆽঅ҅ӧᚆ๜๛ׯᗝԧ̶

    य़ਹտݎሿ֦҅਍౜Ӟӻದ๞຾ԏݸ҅਍ෛጱտஉள҅ᘒӬԞᚆݎሿ՜ժጱᦡᦇ௏మ޾ದૣ፥ጱஉૣ

    ও҅Ԟ௛ᚆತکፘ֒ᅩ҅޾ᦏ֦చݫጱᅩ̶

    ੪೭Doug Lea ٟጱAbstractQueuedSynchronizerҁAQS҂๶᧔҅՜ٟԧӞᤈդᎱ֦҅ݢᚆ፡پॠ಍ᚆ

    ፡౜҅य़֬ժጱ௏మฎ፥ጱᇍ̶

    ౯፡რᎱํ෸ײԞ१ዢ҅֕ฎ݄ᨕྈӞӥ҅ᛔ૩ቘᥴӞӥ҅ᑱᆐ௢ᆐय़ఉጱ෸ײᥧ஑Ӟڔ݈உ꧊̶

    ਍ԟ੪ฎӞ๵෸ᘒᮟᮟੋཻ҅෸ᘒ୏ሾय़ᒞጱ᪠҅य़ਹےရ҅౯ժ౮ᳩ᪠ӤӞ᩸و̶ۣ

    ౯ฎයӰ҅Ӟӻࣁ԰ᘶᗑᝌӬ؎ኞጱૡٍՈ̶

    ๋অጱىᔮฎ԰ፘ౮੪҅य़ਹጱ̿ӣᬳ̀੪ฎӰӰڠ֢ጱ๋य़ۖێ҅౯ժӥ๗ᥠѺ

    ဳғইຎ๜ᓤܗਮํձ֜Კ᧏޾ୌᦓཻ҅ᬨՈ಍ժኸ᥺֦҅ள᧔ݙᦾࠡѺ

    "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +

    "return nil;" +

    "end; " +

    //᭗ᬦhincrby᭓ٺ1ጱොୗ҅᯽නӞེᲁ

    //ᝑۃེ֟හय़ԭ0 ҅ڞڬෛᬦ๗෸ᳵ

    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); "

    +

    "if (counter > 0) then " +

    "redis.call('pexpire', KEYS[1], ARGV[2]); " +

    "return 0; " +

    //ވڞᦤกᲁ૪ᕪ᯽න҅ڢᴻkeyଚݎ૲ᲁ᯽නጱၾ௳

    "else " +

    "redis.call('del', KEYS[1]); " +

    "redis.call('publish', KEYS[2], ARGV[1]); " +

    "return 1; "+

    "end; " +

    "return nil;",

    Arrays.asList(getName(), getChannelName()),

    LockPubSub.unlockMessage, internalLockLeaseTime,

    getLockName(threadId));


    }

    ෈ᒍ೮ᖅๅෛ҅ݢզஙמ൤ᔱ̿ ӣॡৼයӰ ̀ᒫӞ෸ᳵᴅ᧛҅ࢧ॔̓ᩒා̈́̓ᶎᦶ̈́̓ᓌ

    ܲ̈́ํ౯ٵ॓ጱӞᕚय़ܯᶎᦶᩒා޾ᓌܲཛྷ຃҅๜෈ GitHub https://github.com/JavaFamily

    ૪ᕪත୯҅ํय़ܯᶎᦶਠෆᘍᅩཻ҅ᬨStar̶

    ֦Ꭳ᭲ጱ᩼ग़֦҅ӧᎣ᭲ጱ᩼ग़


    本文整理自实践经验,如有问题欢迎交流讨论。


    评论