人妖在线一区,国产日韩欧美一区二区综合在线,国产啪精品视频网站免费,欧美内射深插日本少妇

新聞動態(tài)

使用Redis實(shí)現(xiàn)分布式鎖的方法

發(fā)布日期:2022-07-15 19:28 | 文章來源:腳本之家

Redis 中的分布式鎖如何使用

分布式鎖的使用場景

為了保證我們線上服務(wù)的并發(fā)性和安全性,目前我們的服務(wù)一般拋棄了單體應(yīng)用,采用的都是擴(kuò)展性很強(qiáng)的分布式架構(gòu)。

對于可變共享資源的訪問,同一時(shí)刻,只能由一個(gè)線程或者進(jìn)程去訪問操作。這時(shí)候我們就需要做個(gè)標(biāo)識,如果當(dāng)前有線程或者進(jìn)程在操作共享變量,我們就做個(gè)標(biāo)記,標(biāo)識當(dāng)前資源正在被操作中, 其它的線程或者進(jìn)程,就不能進(jìn)行操作了。當(dāng)前操作完成之后,刪除標(biāo)記,這樣其他的線程或者進(jìn)程,就能來申請共享變量的操作。通過上面的標(biāo)記來保證同一時(shí)刻共享變量只能由一個(gè)線程或者進(jìn)行持有。

對于單體應(yīng)用:多個(gè)線程之間訪問可變共享變量,比較容易處理,可簡單使用內(nèi)存來存儲標(biāo)示即可;

分布式應(yīng)用:這種場景下比較麻煩,因?yàn)槎鄠€(gè)應(yīng)用,部署的地址可能在不同的機(jī)房,一個(gè)在北京一個(gè)在上海。不能簡單的存儲標(biāo)示在內(nèi)存中了,這時(shí)候需要使用公共內(nèi)存來記錄該標(biāo)示,栗如 Redis,MySQL 。。。

使用 Redis 來實(shí)現(xiàn)分布式鎖

這里來聊聊如何使用 Redis 實(shí)現(xiàn)分布式鎖

Redis 中分布式鎖一般會用 set key value px milliseconds nx 或者 SETNX+Lua來實(shí)現(xiàn)。

因?yàn)?SETNX 命令,需要配合 EXPIRE 設(shè)置過期時(shí)間,Redis 中單命令的執(zhí)行是原子性的,組合命令就需要使用 Lua 才能保證原子性了。

看下如何實(shí)現(xiàn)

使用 set key value px milliseconds nx 實(shí)現(xiàn)

因?yàn)檫@個(gè)命令同時(shí)能夠設(shè)置鍵值和過期時(shí)間,同時(shí)Redis中的單命令都是原子性的,所以加鎖的時(shí)候使用這個(gè)命令即可

func (r *Redis) TryLock(ctx context.Context, key, value string, expire time.Duration) (isGetLock bool, err error) {
	// 使用 set nx
	res, err := r.Do(ctx, "set", key, value, "px", expire.Milliseconds(), "nx").Result()
	if err != nil {
		return false, err
	}
	if res == "OK" {
		return true, nil
	}
	return false, nil
}

SETNX+Lua 實(shí)現(xiàn)

如果使用 SETNX 命令,這個(gè)命令不能設(shè)置過期時(shí)間,需要配合 EXPIRE 命令來使用。

因?yàn)槭怯玫搅藘蓚€(gè)命令,這時(shí)候兩個(gè)命令的組合使用是不能保障原子性的,在一些并發(fā)比較大的時(shí)候,需要配合使用 Lua 腳本來保證命令的原子性。

func tryLockScript() string {
	script := `
		local key = KEYS[1]
		local value = ARGV[1] 
		local expireTime = ARGV[2] 
		local isSuccess = redis.call('SETNX', key, value)
		if isSuccess == 1 then
			redis.call('EXPIRE', key, expireTime)
			return "OK"
		end
		return "unLock"    `
	return script
}
func (r *Redis) TryLock(ctx context.Context, key, value string, expire time.Duration) (isGetLock bool, err error) {
	// 使用 Lua + SETNX
	res, err := r.Eval(ctx, tryLockScript(), []string{key}, value, expire.Seconds()).Result()
	if err != nil {
		return false, err
	}
	if res == "OK" {
		return true, nil
	}
	return false, nil
}

除了上面加鎖兩個(gè)命令的區(qū)別之外,在解鎖的時(shí)候需要注意下不能誤刪除別的線程持有的鎖

為什么會出現(xiàn)這種情況呢,這里來分析下

舉個(gè)栗子

1、線程1獲取了鎖,鎖的過期時(shí)間為1s;

2、線程1完成了業(yè)務(wù)操作,用時(shí)1.5s ,這時(shí)候線程1的鎖已經(jīng)被過期時(shí)間自動釋放了,這把鎖已經(jīng)被別的線程獲取了;

3、但是線程1不知道,接著去釋放鎖,這時(shí)候就會將別的線程的鎖,錯(cuò)誤的釋放掉。

面對這種情況,其實(shí)也很好處理

1、設(shè)置 value 具有唯一性;

2、每次刪除鎖的時(shí)候,先去判斷下 value 的值是否能對的上,不相同就表示,鎖已經(jīng)被別的線程獲取了;

看下代碼實(shí)現(xiàn)

var UnLockErr = errors.New("未解鎖成功")
func unLockScript() string {
	script := `
		local value = ARGV[1] 
		local key = KEYS[1]
		local keyValue = redis.call('GET', key)
		if tostring(keyValue) == tostring(value) then
			return redis.call('DEL', key)
		else
			return 0
		end
    `
	return script
}
func (r *Redis) Unlock(ctx context.Context, key, value string) (bool, error) {
	res, err := r.Eval(ctx, unLockScript(), []string{key}, value).Result()
	if err != nil {
		return false, err
	}
	return res.(int64) != 0, nil
}

代碼可參考lock

上面的這類鎖的最大缺點(diǎn)就是只作用在一個(gè)節(jié)點(diǎn)上,即使 Redis 通過 sentinel 保證高可用,如果這個(gè) master 節(jié)點(diǎn)由于某些原因放生了主從切換,那么就會出現(xiàn)鎖丟失的情況:

1、在 Redis 的 master 節(jié)點(diǎn)上拿到了鎖;

2、但是這個(gè)加鎖的 key 還沒有同步到 slave 節(jié)點(diǎn);

3、master 故障,發(fā)生了故障轉(zhuǎn)移,slave 節(jié)點(diǎn)升級為 master 節(jié)點(diǎn);

4、導(dǎo)致鎖丟失。

針對這種情況如何處理呢,下面來聊聊 Redlock 算法

使用 Redlock 實(shí)現(xiàn)分布式鎖

在 Redis 的分布式環(huán)境中,我們假設(shè)有 N 個(gè) Redis master。這些節(jié)點(diǎn)完全互相獨(dú)立,不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制。我們確保將在 N 個(gè)實(shí)例上使用與在 Redis 單實(shí)例下相同方法獲取和釋放鎖?,F(xiàn)在我們假設(shè)有 5 個(gè) Redis master 節(jié)點(diǎn),同時(shí)我們需要在5臺服務(wù)器上面運(yùn)行這些 Redis 實(shí)例,這樣保證他們不會同時(shí)都宕掉。

為了取到鎖,客戶端營該執(zhí)行以下操作:

1、獲取當(dāng)前Unix時(shí)間,以毫秒為單位。

2、依次嘗試從5個(gè)實(shí)例,使用相同的key和具有唯一性的 value(例如UUID)獲取鎖。當(dāng)向 Redis 請求獲取鎖時(shí),客戶端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在 5-50 毫秒之間。這樣可以避免服務(wù)器端 Redis 已經(jīng)掛掉的情況下,客戶端還在死死地等待響應(yīng)結(jié)果。如果服務(wù)器端沒有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶端應(yīng)該盡快嘗試去另外一個(gè) Redis 實(shí)例請求獲取鎖;

3、客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的 Redis 節(jié)點(diǎn)都取到鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功;

4、如果取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果);

5、如果因?yàn)槟承┰颍@取鎖失?。]有在至少N/2+1個(gè) Redis 實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些 Redis 實(shí)例根本就沒有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶端沒有得到響應(yīng)而導(dǎo)致接下來的一段時(shí)間不能被重新獲取鎖)。

根據(jù)官方的推薦,go 版本中 Redsync 實(shí)現(xiàn)了這一算法,這里看下具體的實(shí)現(xiàn)過程

redsync項(xiàng)目地址

// LockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
func (m *Mutex) LockContext(ctx context.Context) error {
	if ctx == nil {
		ctx = context.Background()
	}
	value, err := m.genValueFunc()
	if err != nil {
		return err
	}
	
	for i := 0; i < m.tries; i++ {
		if i != 0 {
			select {
			case <-ctx.Done():
				// Exit early if the context is done.
				return ErrFailed
			case <-time.After(m.delayFunc(i)):
				// Fall-through when the delay timer completes.
			}
		}
		start := time.Now()
		// 嘗試在所有的節(jié)點(diǎn)中加鎖
		n, err := func() (int, error) {
			ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
			defer cancel()
			return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
				// acquire 加鎖函數(shù)
				return m.acquire(ctx, pool, value)
			})
		}()
		if n == 0 && err != nil {
			return err
		}
		// 如果加鎖節(jié)點(diǎn)書沒有達(dá)到的設(shè)定的數(shù)目
		// 或者鍵值的過期時(shí)間已經(jīng)到了
		// 在所有的節(jié)點(diǎn)中解鎖
		now := time.Now()
		until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
		if n >= m.quorum && now.Before(until) {
			m.value = value
			m.until = until
			return nil
		}
		_, err = func() (int, error) {
			ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
			defer cancel()
			return m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
				// 解鎖函數(shù)
				return m.release(ctx, pool, value)
			})
		}()
		if i == m.tries-1 && err != nil {
			return err
		}
	}
	return ErrFailed
}
// 遍歷所有的節(jié)點(diǎn),并且在每個(gè)節(jié)點(diǎn)中執(zhí)行傳入的函數(shù)
func (m *Mutex) actOnPoolsAsync(actFn func(redis.Pool) (bool, error)) (int, error) {
	type result struct {
		Status bool
		Err    error
	}
	ch := make(chan result)
	// 執(zhí)行傳入的函數(shù)
	for _, pool := range m.pools {
		go func(pool redis.Pool) {
			r := result{}
			r.Status, r.Err = actFn(pool)
			ch <- r
		}(pool)
	}
	n := 0
	var err error
	// 計(jì)算執(zhí)行成功的節(jié)點(diǎn)數(shù)目
	for range m.pools {
		r := <-ch
		if r.Status {
			n++
		} else if r.Err != nil {
			err = multierror.Append(err, r.Err)
		}
	}
	return n, err
}
// 手動解鎖的lua腳本
var deleteScript = redis.NewScript(1, `
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("DEL", KEYS[1])
	else
		return 0
	end
`)
// 手動解鎖
func (m *Mutex) release(ctx context.Context, pool redis.Pool, value string) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	status, err := conn.Eval(deleteScript, m.name, value)
	if err != nil {
		return false, err
	}
	return status != int64(0), nil
}

分析下思路

1、遍歷所有的節(jié)點(diǎn),然后嘗試在所有的節(jié)點(diǎn)中執(zhí)行加鎖的操作;

2、收集加鎖成功的節(jié)點(diǎn)數(shù),如果沒有達(dá)到指定的數(shù)目,釋放剛剛添加的鎖;

關(guān)于 Redlock 的缺點(diǎn)可參見

How to do distributed locking

鎖的續(xù)租

Redis 中分布式鎖還有一個(gè)問題就是鎖的續(xù)租問題,當(dāng)鎖的過期時(shí)間到了,但是業(yè)務(wù)的執(zhí)行時(shí)間還沒有完成,這時(shí)候就需要對鎖進(jìn)行續(xù)租了

續(xù)租的流程

1、當(dāng)客戶端加鎖成功后,可以啟動一個(gè)定時(shí)的任務(wù),每隔一段時(shí)間,檢查業(yè)務(wù)是否完成,未完成,增加 key 的過期時(shí)間;

2、這里判斷業(yè)務(wù)是否完成的依據(jù)是:

  • 1、這個(gè) key 是否存在,如果 key 不存在了,就表示業(yè)務(wù)已經(jīng)執(zhí)行完成了,也就不需要進(jìn)行續(xù)租操作了;
  • 2、同時(shí)需要校驗(yàn)下 value 值,如果 value 對應(yīng)的值和之前寫入的值不同了,說明當(dāng)前鎖已經(jīng)被別的線程獲取了;

看下 redsync 中續(xù)租的實(shí)現(xiàn)

// Extend resets the mutex's expiry and returns the status of expiry extension.
func (m *Mutex) Extend() (bool, error) {
	return m.ExtendContext(nil)
}
// ExtendContext resets the mutex's expiry and returns the status of expiry extension.
func (m *Mutex) ExtendContext(ctx context.Context) (bool, error) {
	start := time.Now() 
	// 嘗試在所有的節(jié)點(diǎn)中加鎖
	n, err := m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
		return m.touch(ctx, pool, m.value, int(m.expiry/time.Millisecond))
	})
	if n < m.quorum {
		return false, err
	}
	// 判斷下鎖的過期時(shí)間
	now := time.Now()
	until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
	if now.Before(until) {
		m.until = until
		return true, nil
	}
	return false, ErrExtendFailed
}
var touchScript = redis.NewScript(1, `
	// 需要先比較下當(dāng)前的value值
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("PEXPIRE", KEYS[1], ARGV[2])
	else
		return 0
	end
`)
func (m *Mutex) touch(ctx context.Context, pool redis.Pool, value string, expiry int) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	status, err := conn.Eval(touchScript, m.name, value, expiry)
	if err != nil {
		return false, err
	}
	return status != int64(0), nil
}

1、鎖的續(xù)租需要客戶端去監(jiān)聽和操作,啟動一個(gè)定時(shí)器,固定時(shí)間來調(diào)用續(xù)租函數(shù)給鎖續(xù)租;

2、每次續(xù)租操作的時(shí)候需要匹配下當(dāng)前的 value 值,因?yàn)殒i可能已經(jīng)被當(dāng)前的線程釋放了,當(dāng)前的持有者可能是別的線程;

看看 SETEX 的源碼

SETEX 能保證只有在 key 不存在時(shí)設(shè)置 key 的值,那么這里來看看,源碼中是如何實(shí)現(xiàn)的呢

// https://github.com/redis/redis/blob/7.0/src/t_string.c#L78
// setGenericCommand()函數(shù)是以下命令: SET, SETEX, PSETEX, SETNX.的最底層實(shí)現(xiàn)
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    ...
    found = (lookupKeyWrite(c->db,key) != NULL);
    // 這里是 SETEX 實(shí)現(xiàn)的重點(diǎn)
	// 如果nx,并且在數(shù)據(jù)庫中找到了這個(gè)值就返回
	// 如果是 xx,并且在數(shù)據(jù)庫中沒有找到鍵值就會返回
	
	// 因?yàn)?Redis 中的命令執(zhí)行都是單線程操作的
	// 所以命令中判斷如果存在就返回,能夠保證正確性,不會出現(xiàn)并發(fā)訪問的問題
    if ((flags & OBJ_SET_NX && found) ||
        (flags & OBJ_SET_XX && !found))
    {
        if (!(flags & OBJ_SET_GET)) {
            addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
        }
        return;
    }
    ...
}

1、命令的實(shí)現(xiàn)里面加入了鍵值是否存在的判斷,來保證 NX 只有在 key 不存在時(shí)設(shè)置 key 的值;

2、因?yàn)?Redis 中總是一個(gè)線程處理命令的執(zhí)行,單命令是能夠保證原子性,不會出現(xiàn)并發(fā)的問題。

為什么 Redis 可以用來做分布式鎖

分布式鎖需要滿足的特性

  • 互斥性:在任意時(shí)刻,對于同一個(gè)鎖,只有一個(gè)客戶端能持有,從而保證一個(gè)共享資源同一時(shí)間只能被一個(gè)客戶端操作;
  • 安全性:即不會形成死鎖,當(dāng)一個(gè)客戶端在持有鎖的期間崩潰而沒有主動解鎖的情況下,其持有的鎖也能夠被正確釋放,并保證后續(xù)其它客戶端能加鎖;
  • 可用性:當(dāng)提供鎖服務(wù)的節(jié)點(diǎn)發(fā)生宕機(jī)等不可恢復(fù)性故障時(shí),“熱備” 節(jié)點(diǎn)能夠接替故障的節(jié)點(diǎn)繼續(xù)提供服務(wù),并保證自身持有的數(shù)據(jù)與故障節(jié)點(diǎn)一致。
  • 對稱性:對于任意一個(gè)鎖,其加鎖和解鎖必須是同一個(gè)客戶端,即客戶端 A 不能把客戶端 B 加的鎖給解了。

那么 Redis 對上面的特性是如何支持的呢?

1、Redis 中命令的執(zhí)行都是單線程的,雖然在 Redis6.0 的版本中,引入了多線程來處理 IO 任務(wù),但是命令的執(zhí)行依舊是單線程處理的;

2、單線程的特點(diǎn),能夠保證命令的執(zhí)行的是不存在并發(fā)的問題,同時(shí)命令執(zhí)行的原子性也能得到保證;

3、Redis 中提供了針對 SETNX 這樣的命令,能夠保證同一時(shí)刻是只會有一個(gè)請求執(zhí)行成功,提供互斥性的保障;

4、Redis 中也提供了 EXPIRE 超時(shí)釋放的命令,可以實(shí)現(xiàn)鎖的超時(shí)釋放,避免死鎖的出現(xiàn);

5、高可用,針對如果發(fā)生主從切換,數(shù)據(jù)丟失的情況,Redis 引入了 RedLock 算法,保證了 Redis 中主要大部分節(jié)點(diǎn)正常運(yùn)行,鎖就可以正常運(yùn)行;

6、Redis 中本身沒有對鎖提供續(xù)期的操作,不過一些第三方的實(shí)現(xiàn)中實(shí)現(xiàn)了 Redis 中鎖的續(xù)期,類似 使用 java 實(shí)現(xiàn)的 Redisson,使用 go 實(shí)現(xiàn)的 redsync,當(dāng)然自己實(shí)現(xiàn)也不是很難,實(shí)現(xiàn)過程可參見上文。

總體來說,Redis 中對分布式鎖的一些特性都提供了支持,使用 Redis 實(shí)現(xiàn)分布式鎖,是一個(gè)不錯(cuò)的選擇。

分布式鎖如何選擇

1、如果業(yè)務(wù)規(guī)模不大,qps 很小,使用 Redis,etcd,ZooKeeper 去實(shí)現(xiàn)分布式鎖都不會有問題,就看公司了基礎(chǔ)架構(gòu)了,如果有現(xiàn)成的 Redis,etcd,ZooKeeper 直接用就可以了;

2、Redis 中分布式鎖有一定的安全隱患,如果業(yè)務(wù)中對安全性要求很高,那么 Redis 可能就不適合了,etcd 或者 ZooKeeper 就比較合適了;

3、如果系統(tǒng) qps 很大,但是可以容忍一些錯(cuò)誤,那么 Redis 可能就更合適了,畢竟 etcd或者ZooKeeper 背面往往都是較低的吞吐量和較高的延遲。

總結(jié)

1、在分布式的場景下,使用分布式鎖是我們經(jīng)常遇到的一種場景;

2、使用 Redis 實(shí)現(xiàn)鎖是個(gè)不錯(cuò)的選擇,Redis 的單命令的執(zhí)行是原子性的同時(shí)借助于 Lua 也可以很容易的實(shí)現(xiàn)組合命令的原子性;

3、針對分布式場景下主從切換,數(shù)據(jù)同步不及時(shí)的情況,redis 中引入了 redLock 來處理分布式鎖;

4、根據(jù) martin 的描述,redLock 是繁重的,且存在安全性,不過我們可以根據(jù)自己的業(yè)務(wù)場景做出判斷;

5、需要注意的是在設(shè)置分布式鎖的時(shí)候需要設(shè)置 value 的唯一性,并且每次主動刪除鎖的時(shí)候需要匹配下 value 的正確性,避免誤刪除其他線程的鎖;

參考

【Redis核心技術(shù)與實(shí)戰(zhàn)】https://time.geekbang.org/column/intro/100056701
【Redis設(shè)計(jì)與實(shí)現(xiàn)】https://book.douban.com/subject/25900156/
【Redis 的學(xué)習(xí)筆記】https://github.com/boilingfrog/Go-POINT/tree/master/redis
【Redis 分布式鎖】https://redis.io/docs/reference/patterns/distributed-locks/
【How to do distributed locking】https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
【etcd 實(shí)現(xiàn)分布式鎖】https://www.cnblogs.com/ricklz/p/15033193.html#分布式鎖
【Redis中的原子操作(3)-使用Redis實(shí)現(xiàn)分布式鎖】https://boilingfrog.github.io/2022/06/15/ Redis中的原子操作 (3)-使用Redis實(shí)現(xiàn)分布式鎖/

到此這篇關(guān)于使用Redis實(shí)現(xiàn)分布式鎖的方法的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!

國外穩(wěn)定服務(wù)器

版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。

實(shí)時(shí)開通

自選配置、實(shí)時(shí)開通

免備案

全球線路精選!

全天候客戶服務(wù)

7x24全年不間斷在線

專屬顧問服務(wù)

1對1客戶咨詢顧問

在線
客服

在線客服:7*24小時(shí)在線

客服
熱線

400-630-3752
7*24小時(shí)客服服務(wù)熱線

關(guān)注
微信

關(guān)注官方微信
頂部