java分布式锁
创始人
2024-03-22 19:44:30

文章目录

  • 1.基于数据库实现分布式锁
  • 2. 基于缓存(redis)实现分布式锁
    • 2.1 基于redis实现的分布式锁
      • 2.1.1 配置类
      • 2.1.2 分布式锁实现
      • 2.1.3 看门狗具体实现
    • 2.2 基于Redission实现
      • 2.2.1导入redssion包
      • 2.1.2 创建配置类
      • 2.1.3 实际业务代码的实现
  • 3. 基于Zookeeper实现分布式锁
    • 3.1代码实现
      • 3.1.1 导包
      • 3.1.2 ZK配置类
      • 3.1.3 实体类
      • 3.1.4 实际业务代码实现

1.基于数据库实现分布式锁

数据库实现分布式锁方式比较多,如悲观锁(查询时增加for update)、乐观锁(通过version字段)、增加一个表记录锁信息等。因为依赖于数据库,比较好理解,但是也存在一些问题。
如悲观锁在某些情况下可能会锁表而不是锁行,乐观锁可能需要多次重试,以及操作数据库的性能开销等等,所以基于数据库的分布式锁不做过多研究,因为我看来基本上不会用到。

2. 基于缓存(redis)实现分布式锁

2.1 基于redis实现的分布式锁

2.1.1 配置类

package com.mashibing.lock;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;@Configuration
@PropertySource("classpath:application.properties")
public class RedisConfig {@Value("${redis.host}")private String host;@Value("${redis.port}")private int port;@Value("${redis.timeout}")private int timeout;@Value("${redis.maxIdle}")private int maxIdle;@Value("${redis.maxWaitMillis}")private int maxWaitMillis;@Value("${redis.blockWhenExhausted}")private Boolean blockWhenExhausted;@Value("${redis.JmxEnabled}")private Boolean JmxEnabled;@Beanpublic JedisPool jedisPoolFactory() {JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();jedisPoolConfig.setMaxIdle(maxIdle);jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);// 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认truejedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);// 是否启用pool的jmx管理功能, 默认truejedisPoolConfig.setJmxEnabled(JmxEnabled);JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);return jedisPool;}
}

2.1.2 分布式锁实现

package com.mashibing.lock;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** 分布式锁的实现*/
@Component
public class RedisDistLock implements Lock {private final static int LOCK_TIME = 5*1000;//失效时间private final static String RS_DISTLOCK_NS = "tdln:"; //加锁的key的前缀/*if redis.call('get',KEYS[1])==ARGV[1] thenreturn redis.call('del', KEYS[1])else return 0 end*///释放锁的时候,确保原子。lua脚本:确保  释放锁的线程就是加锁的线程,不能被线程的线程无脑调用释放private final static String RELEASE_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('del', KEYS[1])\n" +"    else return 0 end";/*保存每个线程的独有的ID值*/private ThreadLocal lockerId = new ThreadLocal<>();/*解决锁的重入*/private Thread ownerThread;private String lockName = "lock";@Autowiredprivate JedisPool jedisPool;public String getLockName() {return lockName;}public void setLockName(String lockName) {this.lockName = lockName;}public Thread getOwnerThread() {return ownerThread;}public void setOwnerThread(Thread ownerThread) {//加锁成功,就会把抢到锁的线程进行保存this.ownerThread = ownerThread;}@Overridepublic void lock() { //redis的分布式锁while(!tryLock()){try {Thread.sleep(100); //每隔100ms 都会去尝试加锁} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException("不支持可中断获取锁!");}@Overridepublic boolean tryLock() {Thread t = Thread.currentThread();if(ownerThread==t){/*说明本线程持有锁*/return true;}else if(ownerThread!=null){/*本进程里有其他线程持有分布式锁*/return false;}Jedis jedis = jedisPool.getResource();try {String id = UUID.randomUUID().toString();SetParams params = new SetParams();params.px(LOCK_TIME);params.nx();synchronized (this){/*线程们,本地抢锁*/if((ownerThread==null)&&"OK".equals(jedis.set(RS_DISTLOCK_NS+lockName,id,params))){lockerId.set(id);setOwnerThread(t);return true;}else{return false;}}} catch (Exception e) {throw new RuntimeException("分布式锁尝试加锁失败!");} finally {jedis.close();}}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new UnsupportedOperationException("不支持等待尝试获取锁!");}@Overridepublic void unlock() {if(ownerThread!=Thread.currentThread()) {throw new RuntimeException("试图释放无所有权的锁!");}Jedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,Arrays.asList(RS_DISTLOCK_NS+lockName),Arrays.asList(lockerId.get()));if(result.longValue()!=0L){System.out.println("Redis上的锁已释放!");}else{System.out.println("Redis上的锁释放失败!");}} catch (Exception e) {throw new RuntimeException("释放锁失败!",e);} finally {if(jedis!=null) jedis.close();lockerId.remove();setOwnerThread(null);System.out.println("本地锁所有权已释放!");}}@Overridepublic Condition newCondition() {throw new UnsupportedOperationException("不支持等待通知操作!");}}

这样实现的分布式锁会产生的问题是,当业务代码消耗的时间大于redis的过期时间,业务代码还没执行完,锁已经释放

解决方案:加入看门狗,就是判断业务代码是否执行完,没执行完, 在redis失效前100毫秒,给redis续期

2.1.3 看门狗具体实现

ItemVo

package com.mashibing.lock.rdl;import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;/****类说明:存放到延迟队列的元素,比标准的delay的实现要提前一点时间*/
public class ItemVo implements Delayed{/*到期时刻  20:00:35,234*/private long activeTime;/*业务数据,泛型*/private T data;/*传入的数值代表过期的时长,单位毫秒,需要乘1000转换为毫秒和到期时间* 同时提前100毫秒续期,具体的时间可以自己决定*/public ItemVo(long expirationTime, T data) {super();this.activeTime = expirationTime+System.currentTimeMillis()-100;this.data = data;}public long getActiveTime() {return activeTime;}public T getData() {return data;}/*** 返回元素到激活时刻的剩余时长*/public long getDelay(TimeUnit unit) {long d = unit.convert(this.activeTime- System.currentTimeMillis(),unit);return d;}/**按剩余时长排序*/public int compareTo(Delayed o) {long d = (getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS));if (d==0){return 0;}else{if (d<0){return -1;}else{return  1;}}}}

LockItem

package com.mashibing.lock.rdl;
/****类说明:Redis的key-value结构*/
public class LockItem {private final String key;private final String value;public LockItem(String key, String value) {this.key = key;this.value = value;}public String getKey() {return key;}public String getValue() {return value;}
}

RedisDistLockWithDog

package com.mashibing.lock.rdl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;import javax.annotation.PreDestroy;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** 分布式锁,附带看门狗线程的实现:加锁,保持锁1秒*/
@Component
public class RedisDistLockWithDog implements Lock {private final static int LOCK_TIME = 1*1000;private final static String LOCK_TIME_STR = String.valueOf(LOCK_TIME);private final static String RS_DISTLOCK_NS = "tdln2:";/*if redis.call('get',KEYS[1])==ARGV[1] thenreturn redis.call('del', KEYS[1])else return 0 end*/private final static String RELEASE_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('del', KEYS[1])\n" +"    else return 0 end";/*还有并发问题,考虑ThreadLocal*/private ThreadLocal lockerId = new ThreadLocal<>();private Thread ownerThread;private String lockName = "lock";@Autowiredprivate JedisPool jedisPool;public String getLockName() {return lockName;}public void setLockName(String lockName) {this.lockName = lockName;}public Thread getOwnerThread() {return ownerThread;}public void setOwnerThread(Thread ownerThread) {this.ownerThread = ownerThread;}@Overridepublic void lock() {while(!tryLock()){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException("不支持可中断获取锁!");}@Overridepublic boolean tryLock() {Thread t=Thread.currentThread();/*说明本线程正在持有锁*/if(ownerThread==t) {return true;}else if(ownerThread!=null){/*说明本进程中有别的线程正在持有分布式锁*/return false;}Jedis jedis = null;try {jedis = jedisPool.getResource();/*每一个锁的持有人都分配一个唯一的id,也可采用snowflake算法*/String id = UUID.randomUUID().toString();SetParams params = new SetParams();params.px(LOCK_TIME); //加锁时间1sparams.nx();synchronized (this){if ((ownerThread==null)&&"OK".equals(jedis.set(RS_DISTLOCK_NS+lockName,id,params))) {lockerId.set(id);setOwnerThread(t);if(expireThread == null){//看门狗线程启动expireThread = new Thread(new ExpireTask(),"expireThread");expireThread.setDaemon(true);expireThread.start();}//往延迟阻塞队列中加入元素(让看门口可以在过期之前一点点的时间去做锁的续期)delayDog.add(new ItemVo<>((int)LOCK_TIME,new LockItem(lockName,id)));System.out.println(Thread.currentThread().getName()+"已获得锁----");return true;}else{System.out.println(Thread.currentThread().getName()+"无法获得锁----");return false;}}} catch (Exception e) {throw new RuntimeException("分布式锁尝试加锁失败!",e);} finally {jedis.close();}}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new UnsupportedOperationException("不支持等待尝试获取锁!");}@Overridepublic void unlock() {if(ownerThread!=Thread.currentThread()) {throw new RuntimeException("试图释放无所有权的锁!");}Jedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,Arrays.asList(RS_DISTLOCK_NS+lockName),Arrays.asList(lockerId.get()));System.out.println(result);if(result.longValue()!=0L){System.out.println("Redis上的锁已释放!");}else{System.out.println("Redis上的锁释放失败!");}} catch (Exception e) {throw new RuntimeException("释放锁失败!",e);} finally {if(jedis!=null) jedis.close();lockerId.remove();setOwnerThread(null);}}@Overridepublic Condition newCondition() {throw new UnsupportedOperationException("不支持等待通知操作!");}/*看门狗线程*/private Thread expireThread;//通过delayDog 避免无谓的轮询,减少看门狗线程的轮序次数   阻塞延迟队列   刷1  没有刷2private static DelayQueue> delayDog = new DelayQueue<>();//续锁逻辑:判断是持有锁的线程才能续锁private final static String DELAY_LOCK_LUA ="if redis.call('get',KEYS[1])==ARGV[1] then\n" +"        return redis.call('pexpire', KEYS[1],ARGV[2])\n" +"    else return 0 end";private class ExpireTask implements Runnable{@Overridepublic void run() {System.out.println("看门狗线程已启动......");while(!Thread.currentThread().isInterrupted()) {try {LockItem lockItem = delayDog.take().getData();//只有元素快到期了才能take到  0.9sJedis jedis = null;try {jedis = jedisPool.getResource();Long result = (Long)jedis.eval(DELAY_LOCK_LUA,Arrays.asList(RS_DISTLOCK_NS+lockItem.getKey ()),Arrays.asList(lockItem.getValue(),LOCK_TIME_STR));if(result.longValue()==0L){System.out.println("Redis上的锁已释放,无需续期!");}else{delayDog.add(new ItemVo<>((int)LOCK_TIME,new LockItem(lockItem.getKey(),lockItem.getValue())));System.out.println("Redis上的锁已续期:"+LOCK_TIME);}} catch (Exception e) {throw new RuntimeException("锁续期失败!",e);} finally {if(jedis!=null) jedis.close();}} catch (InterruptedException e) {System.out.println("看门狗线程被中断");break;}}System.out.println("看门狗线程准备关闭......");}}//    @PostConstruct
//    public void initExpireThread(){
//
//    }@PreDestroypublic void closeExpireThread(){if(null!=expireThread){expireThread.interrupt();}}
}

2.2 基于Redission实现

2.2.1导入redssion包

   org.redissonredisson3.12.3

2.1.2 创建配置类

package com.mashibing.redission;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MyRedissionConfig {@Value("${redis.host}")private String host;@Value("${redis.port}")private String port;@Bean(destroyMethod = "shutdown")public RedissonClient redisson(){//1、创建配置Config config = new Config();config.useSingleServer().setAddress("redis://"+host+":"+port);//2、根据config配置创建出RedissonClient的实例RedissonClient redissonClient= Redisson.create(config);return redissonClient;}
}

2.1.3 实际业务代码的实现

package com.mashibing.service;import com.mashibing.dao.ShopGoodsMapper;
import com.mashibing.dao.shopGoodsUniqueMapper;
import com.mashibing.lock.RedisDistLock;
import com.mashibing.model.ShopGoods;
import com.mashibing.model.shopGoodsUnique;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;@Service
@Transactional
public class GoodsServiceRedisImpl {private static final Logger logger = LoggerFactory.getLogger(GoodsServiceRedisImpl.class);@Autowiredprivate ShopGoodsMapper shopGoodsMapper;@Autowiredprivate shopGoodsUniqueMapper shopGoodsUniqueMapper;//Redis分布式锁@Autowiredprivate RedisDistLock redisDistLock;@Autowiredprivate RedissonClient redissonClient;//Zookeeper分布式锁@Autowiredprivate CuratorFramework  curatorFramework;
//    static InterProcessLock lock ;//多个应用,分布式部署,这里需要使用分布式锁类解决问题public  int updateGoods(long orderId, long goodsId, int goodsNumber){
//        synchronized (this){
//            if(lock ==null){ //多线程安全问题
//                lock =  new InterProcessMutex(curatorFramework,"/locks2");
//            }
//        }int ireturn =-1;RLock lock = redissonClient.getLock("RD-LOCK");lock.lock(1, TimeUnit.SECONDS);try{//redisson实现分布式锁//            redisDistLock.lock();
//           zookeeper实现的分布式锁(临时、序号节点,包括监听机制)
//            if(lock.acquire(5, TimeUnit.SECONDS)){ShopGoods shopGoods =shopGoodsMapper.selectByPrimaryKey(goodsId);Integer goodnumber = shopGoods.getGoodsNumber()-goodsNumber;shopGoods.setGoodsNumber(goodnumber);if(shopGoodsMapper.updateByPrimaryKey(shopGoods)>=0){//logger.info("修改库存成功:[" + orderId + "]");ireturn =1;}else{logger.error("修改库存失败:[" + orderId + "]");ireturn =-1;}
//            }else {
//                logger.error("修改库存失败:[拿不到zk分布式锁]");
//                ireturn =-1;
//            }}catch (Exception e) {e.printStackTrace();}finally {lock.unlock();
//            redisDistLock.unlock();
//            try {
//                lock.release(); //释放锁
//            } catch (Exception e) {
//                e.printStackTrace();
//            }return ireturn;}}//回退-扣减库存public  int CancelupdateGoods(long orderId, long goodsId, int goodsNumber){try {//去重表中有,才能证明是插入了,所以要回退shopGoodsUnique shopGoodsUnique = new shopGoodsUnique();shopGoodsUnique.setOrderId(orderId);shopGoodsUniqueMapper.insert(shopGoodsUnique);}catch (Exception e) {ShopGoods shopGoods =shopGoodsMapper.selectByPrimaryKey(goodsId);Integer goodnumber = shopGoods.getGoodsNumber()+goodsNumber;shopGoods.setGoodsNumber(goodnumber);if(shopGoodsMapper.updateByPrimaryKey(shopGoods)>=0){//logger.info("修改库存成功:[" + orderId + "]");return 1;}else{logger.error("回退库存失败:[" + orderId + "]");return -1;}}return 1;}}

3. 基于Zookeeper实现分布式锁

原理:创建临时节点(就是基于客户端与服务端的session,断开就会删除),第一个线程进来获得锁,第二个线程利用watch机制监听第一个线程,第一个线程结束就获得锁,以此类推

3.1代码实现

3.1.1 导包

  org.apache.curatorcurator-framework5.2.1org.apache.curatorcurator-recipes5.2.1org.apache.zookeeperzookeeper3.8.0

3.1.2 ZK配置类

package com.mashibing.lock.zk;import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CuratorConfig {@AutowiredWrapperZK wrapperZK;//这里返回一个zk的客户端的bean@Beanpublic CuratorFramework curatorFramework(){RetryPolicy retryPolicy = new ExponentialBackoffRetry(wrapperZK.getElapsedTimeMs(), wrapperZK.getRetryCount());CuratorFramework client= CuratorFrameworkFactory.newClient(wrapperZK.getConnectString(),wrapperZK.getSessionTimeoutMs(),wrapperZK.getConnectionTimeoutMs(),retryPolicy);client.start();return client;}
}

3.1.3 实体类

package com.mashibing.lock.zk;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "curator")
public class WrapperZK {private int retryCount;private int elapsedTimeMs;private String connectString;private int sessionTimeoutMs;private int connectionTimeoutMs;
}

3.1.4 实际业务代码实现

package com.mashibing.service;import com.mashibing.dao.ShopGoodsMapper;
import com.mashibing.dao.shopGoodsUniqueMapper;
import com.mashibing.lock.RedisDistLock;
import com.mashibing.lock.rdl.RedisDistLockWithDog;
import com.mashibing.model.ShopGoods;
import com.mashibing.model.shopGoodsUnique;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.concurrent.TimeUnit;/***@author 李瑾老师**类说明:订单相关的服务*/
@Service
@Transactional
public class GoodsServiceImpl {private static final Logger logger = LoggerFactory.getLogger(GoodsServiceImpl.class);@Autowiredprivate ShopGoodsMapper shopGoodsMapper;@Autowiredprivate shopGoodsUniqueMapper shopGoodsUniqueMapper;//Redis分布式锁@Autowiredprivate RedisDistLock redisDistLock;//Zookeeper分布式锁@Autowiredprivate CuratorFramework  curatorFramework;static InterProcessLock lock ;//多个应用,分布式部署,这里需要使用分布式锁类解决问题public  int updateGoods(long orderId, long goodsId, int goodsNumber){synchronized (this){if(lock ==null){ //多线程安全问题lock =  new InterProcessMutex(curatorFramework,"/locks2");}}int ireturn =-1;try{//redisDistLock.lock();//zookeeper实现的分布式锁(临时、序号节点,包括监听机制)if(lock.acquire(5, TimeUnit.SECONDS)){ShopGoods shopGoods =shopGoodsMapper.selectByPrimaryKey(goodsId);Integer goodnumber = shopGoods.getGoodsNumber()-goodsNumber;shopGoods.setGoodsNumber(goodnumber);if(shopGoodsMapper.updateByPrimaryKey(shopGoods)>=0){//logger.info("修改库存成功:[" + orderId + "]");ireturn =1;}else{logger.error("修改库存失败:[" + orderId + "]");ireturn =-1;}}else {logger.error("修改库存失败:[拿不到zk分布式锁]");ireturn =-1;}}catch (Exception e) {e.printStackTrace();}finally {//redisDistLock.unlock();try {lock.release(); //释放锁} catch (Exception e) {e.printStackTrace();}return ireturn;}}//回退-扣减库存public  int CancelupdateGoods(long orderId, long goodsId, int goodsNumber){try {//去重表中有,才能证明是插入了,所以要回退shopGoodsUnique shopGoodsUnique = new shopGoodsUnique();shopGoodsUnique.setOrderId(orderId);shopGoodsUniqueMapper.insert(shopGoodsUnique);}catch (Exception e) {ShopGoods shopGoods =shopGoodsMapper.selectByPrimaryKey(goodsId);Integer goodnumber = shopGoods.getGoodsNumber()+goodsNumber;shopGoods.setGoodsNumber(goodnumber);if(shopGoodsMapper.updateByPrimaryKey(shopGoods)>=0){//logger.info("修改库存成功:[" + orderId + "]");return 1;}else{logger.error("回退库存失败:[" + orderId + "]");return -1;}}return 1;}}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...