• java分布式锁的实现及在springboot中的应用


    redis

    分布式实现原理

    SET resource_name my_random_value NX PX 30000
    
    • NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil
    • PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
    • random_value 随机值,判断 value 一样才删除

    jedis实现

    	<dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>2.9.2</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
                <version>2.5.0</version>
            </dependency>
    
    public class JedisUtil {
    
        private static JedisPool jedisPool;
        static {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10000);
            jedisPoolConfig.setMaxWaitMillis(10000);
            jedisPoolConfig.setMaxIdle(1000);
            jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,100000);
    
        }
    
        public static Jedis getRerouse(){
            return  jedisPool.getResource();
        }
    
    
    }
    
    public class JedisDistributeLock {
    
        private static final Logger logger = LoggerFactory.getLogger(JedisDistributeLock.class);
    
        private static final String SUCCESS ="OK";
        private static final String SET_IF_NOT_EXIST ="NX";
        private static final String SET_WITH_EXPIRE_TIME ="EX";
    
        //释放锁成功标示
        private static final Long RELEASE_SUCCESS = 1L;
    
        //获取锁时的睡眠等待时间片,单位毫秒
        private static final long SLEEP_PER = 5;
    
        //默认过期时间
        public static final int DEFAULT_EXPIRE_1000_Milliseconds = 1000;
    
        private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        private void setThreadLocal(String uuid){
            threadLocal.set(uuid);
        }
    
        private String getThreadLocal(){
            return threadLocal.get();
        }
    
    
        public void acquire(String key){
            String value = UUID.randomUUID().toString();
            lock(key,value,5000);
            setThreadLocal(value);
        }
    
    
        public void release(String key){
            String value = getThreadLocal();
            unlock(key,value);
        }
    
    
        private  Boolean tryGetLock(Jedis jedis,String key,String requestId,int expireTime){
    
            String result = jedis.set(key,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
            if(SUCCESS.equals(result)){
                return true;
            }
            return false;
        }
    
    
        private  Boolean releaseLock(Jedis jedis,String key,String requestId){
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(key),Collections.singletonList(requestId));
            if(RELEASE_SUCCESS.equals(result)){
                return true;
            }
            return false;
        }
    
        private  void lock(String key,String value,int expireTime){
            try (Jedis jedis = JedisUtil.getRerouse()){
                while(!tryLock(key,value,expireTime)){
                    //缺点,睡眠期间其它线程释放锁不能及时收到
                    try {
                        Thread.sleep(SLEEP_PER);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        private  boolean tryLock(String key,String value,int expireTime){
            try (Jedis jedis = JedisUtil.getRerouse()){
                return  tryGetLock(jedis,key,value,expireTime);
            }
        }
    
        private   void unlock(String key,String value){
            try (Jedis jedis = JedisUtil.getRerouse()){
                releaseLock(jedis,key,value);
            }
        }
    
        public static void main(String[] args) {
            JedisDistributeLock lock = new JedisDistributeLock();
            try {
                lock.acquire("order");
                logger.info("01获取锁");
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            lock.acquire("order");
                            logger.info("02获取锁");
                            lock.release("order");
                            logger.info("02释放锁");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.start();
                Thread.sleep(2000);
                lock.release("order");
                logger.info("01释放锁");
            }catch (Exception e){
                e.printStackTrace();
            }
    
    
    
        }
    }
    

    lettuce结合springboot实现

    springboot2.x后redis客户端默认使用lettuce

     	<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
    @Configuration
    @EnableCaching
    public class RedisConfig {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
            //设置序列化
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            // 配置redisTemplate
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
            redisTemplate.setConnectionFactory(lettuceConnectionFactory);
            RedisSerializer stringSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringSerializer); // key序列化
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
            redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // Hash value序列化
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    
    }
    
    
    @Component
    public class SpringRedisLock {
    
        private static final Logger logger = LoggerFactory.getLogger(SpringRedisLock.class);
    
        private static final RedisScript<Long> lua_lock = new DefaultRedisScript<>("if redis.call("setnx", KEYS[1], KEYS[2]) == 1 then return redis.call("pexpire", KEYS[1], KEYS[3]) else return 0 end", Long.class);
    
        private static final RedisScript<Long> lua_unlock = new DefaultRedisScript<>("if redis.call("get",KEYS[1]) == KEYS[2] then return redis.call("del",KEYS[1]) else return -1 end", Long.class);
    
        @Autowired
        private RedisTemplate redisTemplate;
        Integer timeOut =1;
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 多台机器的情况下,会出现大量的等待,加重redis的压力。 在lock方法上,加入同步关键字。单机同步,多机用redis
        Lock lock = new ReentrantLock();
    
        public void lock(String key ) {
            try {
                lock.lock();
                while(!tryLock(key)){
    
                    redisTemplate.execute(new RedisCallback<Boolean>() {
                        @Override
                        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                            CountDownLatch waiter = new CountDownLatch(1);
                            // 等待通知结果,使用jedis在此处会阻塞
                            connection.subscribe((message, pattern) -> {
                                // 收到通知,不管结果,立刻再次抢锁
                                waiter.countDown();
                            }, (key + "_unlock_channel").getBytes());
                            try {
                                // 等待一段时间,超过这个时间都没收到消息,肯定有问题
                                waiter.await(timeOut, TimeUnit.SECONDS);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            return true; //随便返回一个值都没问题
                        }
                    });
                    logger.info("继续下一次循环");
                }
            }  finally {
                lock.unlock();
            }
    
    
    
        }
    
    
        public boolean tryLock(String key ) {
            String value = UUID.randomUUID().toString();
            List<String> keys = Arrays.asList(key, value, String.valueOf(1000));
            Long result = (Long) redisTemplate.execute(lua_lock,keys);
            if(result==1){
                logger.info(Thread.currentThread().getName() + "获取到锁");
                threadLocal.set(value);
                return true;
            }else{
                return false;
            }
        }
    
    
        public void unlock(String key) {
            //1、 要比对内部的值,同一个线程,才能够去释放锁。 2、 同时发出通知
            String value = threadLocal.get();
            try {
                List<String> keys = Arrays.asList(key, value);
                Long result = (Long) redisTemplate.execute(lua_unlock,keys);
                if(result !=-1){
                    logger.info(Thread.currentThread().getName() + "释放锁");
                    redisTemplate.execute(new RedisCallback() {
                        @Override
                        public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                            redisConnection.publish((key + "_unlock_channel").getBytes(),"".getBytes());
                            return null;
                        }
                    });
                }
            }finally {
                threadLocal.remove();
            }
        }
    
    }
    

    测试类

    @Service
    public class TicketService {
    
        @Autowired
        private SpringRedisLock springRedisLock;
    
        private Long tickets = 0l;
    
        public void buyTicket(String userId) {
            try {
                springRedisLock.lock("ticket");
                System.out.println(userId + "正在买第 " + ++tickets + " 张票");
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                springRedisLock.unlock("ticket");
            }
        }
    }
    
    @Test
        public void testRe(){
            int currency = 200;
            CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
            for (int i = 0; i <currency ; i++) {
                String userId = "Tony" +i;
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName()+"线程准备好");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    ticketService.buyTicket(userId);
                }).start();
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    redission实现

    实现原理如下


    代码

    	<dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.9.1</version>
            </dependency>
    
    public class RedissionLock {
    
        private static final Logger logger = LoggerFactory.getLogger(RedissionLock.class);
    
        private RLock rLock;
    
        private RedissionLock(String key) {
            rLock = getRlock(key);
        }
    
        private RLock getRlock(String key) {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://127.0.0.1:6379");
            RedissonClient client = Redisson.create(config);
            RLock rLock = client.getLock("order");
            return rLock;
        }
    
        public void acquire() {
            rLock.lock();
        }
    
        public void release() {
            rLock.unlock();
        }
    
        public static void main(String[] args) {
            RedissionLock lock = new RedissionLock("order");
            try {
                lock.acquire();
                logger.info("01获取锁");
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            lock.acquire();
                            logger.info("02获取锁");
                            lock.release();
                            logger.info("02释放锁");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.start();
                Thread.sleep(2000);
                lock.release();
                logger.info("01释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    
    

    redlock

    分布式架构中的CAP理论,分布式系统只能同时满足两个: 一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。

    上述redis分布式锁是AP模式,当锁存在的redis节点宕机,可能会被误判为锁失效,或者没有加锁。具体如下:

    1. A客户端请求主节点获取到了锁
    2. 主节点挂掉了,但是还没把锁的信息同步给其他从节点
    3. 由于主节点挂了,这时候开始主从切换,从节点成为主节点继续工作,但是新的主节点上,没有A客户端的加锁信息
    4. 这时候B客户端来加锁,因为目前是一个新的主节点,上面没有其他客户端加锁信息,所以B客户端获取锁成功
    5. 这时候就存在问题了,A和B两个客户端同时都持有锁,同时在执行代码,那么这时候分布式锁就失效了。

    在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

    RedLock算法思想,意思是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。

    redisson已经有对redlock算法封装。具体使用如下:

    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");
    boolean isLock;
    try {
    	isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    	if (isLock) {
    		//TODO if get lock success, do something;
    	}
    } catch (Exception e) {
    } finally {
    	redLock.unlock();
    }
    

    redlock可能失效的场景

    • 某个节点没有持久化导致锁失效,A、B、C、D、E,user01 锁住A、B、C,而后C宕机重启,user02锁住C、D、E,可以使用延时重启(延时时间要结合业务锁失效时间)
    • 某个节点发生时钟跳跃导致锁失效
    • 在锁释放前已经过了失效时间(单机也有这种问题),比如GC、操作系统cpu上下文切换、网络延时

    zookeeper

    临时节点

    多个客户端去创建相同的临时节点,创建成功则表示拿到了锁

    	<dependency>
                <groupId>com.101tec</groupId>
                <artifactId>zkclient</artifactId>
                <version>0.10</version>
            </dependency>
    
    public class ZkLock {
    
        private static final Logger logger = LoggerFactory.getLogger(ZkLock.class);
    
        private ZkClient zkClient;
    
        private String lockPath;
    
        private final static String ROOT_PATH = "/lock/";
    
        private  ZkLock(String lockPath){
            zkClient = new ZkClient("127.0.0.1:2181");
            zkClient.setZkSerializer(new MyZkSerializer());
            this.lockPath = ROOT_PATH + lockPath;
        }
    
    
        public void lock() {
            if(!tryLock()){
                waitForLock();
                lock();
            }
        }
    
        public void unlock() {
            zkClient.delete(lockPath);
    
        }
    
        public boolean tryLock() {
            try {
                zkClient.createEphemeral(lockPath);
            } catch (ZkNodeExistsException e) {
                return false;
            }
            return true;
        }
    
        private void waitForLock(){
            CountDownLatch countDownLatch = new CountDownLatch(1);
            IZkDataListener listener = new IZkDataListener() {
                @Override
                public void handleDataChange(String s, Object o) throws Exception {
                }
                @Override
                public void handleDataDeleted(String s) throws Exception {
                   logger.info(s+ "节点被删除了");
                   countDownLatch.countDown();
                }
            };
            zkClient.subscribeDataChanges(lockPath,listener);
            if(zkClient.exists(lockPath)){
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            zkClient.unsubscribeDataChanges(lockPath,listener);
        }
    
    
        public static void main(String[] args) {
    
            ZkLock zkLock01 = new ZkLock("order");
            ZkLock zkLock02 = new ZkLock("order");
            try {
                zkLock01.lock();
                logger.info("zkLock01获取锁");
                Thread thread = new Thread(new Runnable() {
    
                    public void run() {
                        try {
                            zkLock02.lock();
                            logger.info("zkLock02第一次获取锁");
                            zkLock02.unlock();
                            logger.info("zkLock02第一次释放锁");
                            zkLock02.lock();
                            logger.info("zkLock02第二次获取锁");
                            zkLock02.unlock();
                            logger.info("zkLock02第二次释放锁");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.start();
                Thread.sleep(2000);
                zkLock01.unlock();
                logger.info("zkLock01释放锁");
            }catch (Exception e){
                e.printStackTrace();
            }
    
    
        }
    }
    
    

    临时顺序节点

    多个客户端在一个节点下创建临时顺序节点,按照节点顺序依次获取锁,避免了惊群效应,提高了性能

    public class ZkLock02 {
    
        private static final Logger logger = LoggerFactory.getLogger(ZkLock02.class);
        private String lockPath;
    
        private ZkClient zkClient;
    
        private String beforePath;
    
        private String currentPath;
    
        private final static String ROOT_PATH = "/lock/";
    
    
        public ZkLock02(String path){
            this.lockPath = ROOT_PATH +path;
            zkClient = new ZkClient("127.0.0.1:2181");
            zkClient.setZkSerializer(new MyZkSerializer());
            if(!zkClient.exists(lockPath)){
                try {
                    zkClient.createPersistent(lockPath);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public void unlock() {
            zkClient.delete(currentPath);
    
        }
    
        public boolean tryLock() {
            if(currentPath==null){
                currentPath = zkClient.createEphemeralSequential(lockPath+"/","asd");
            }
            List<String> childs = zkClient.getChildren(lockPath).stream().sorted((a, b)->a.compareTo(b)).collect(Collectors.toList());
            if(currentPath.equals(lockPath +"/" + childs.get(0))){
                return true;
            }else{
                int curIndex = childs.indexOf(currentPath.substring(lockPath.length()+1));
                beforePath = lockPath +"/" + childs.get(curIndex-1);
            }
            return false;
        }
    
        public void lock() {
            if(!tryLock()){
                waitForLock();
                lock();
            }
        }
    
        private void waitForLock(){
            CountDownLatch countDownLatch = new CountDownLatch(1);
            IZkDataListener listener = new IZkDataListener() {
                @Override
                public void handleDataChange(String s, Object o) throws Exception {
                }
                @Override
                public void handleDataDeleted(String s) throws Exception {
                    logger.info(s+ "节点被删除了");
                    countDownLatch.countDown();
                }
            };
            zkClient.subscribeDataChanges(beforePath,listener);
            if(zkClient.exists(beforePath)){
                try {
                    countDownLatch.await();
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            zkClient.unsubscribeDataChanges(beforePath,listener);
    
        }
    
        public static void main(String[] args) {
    
            ZkLock02 zkLock01 = new ZkLock02("order");
            ZkLock02 zkLock02 = new ZkLock02("order");
            ZkLock02 zkLock03 = new ZkLock02("order");
            try {
                zkLock01.lock();
                logger.info("zkLock01获取锁");
                Thread thread = new Thread(new Runnable() {
    
                    public void run() {
                        try {
                            zkLock02.lock();
                            logger.info("zkLock02第一次获取锁");
                            zkLock02.unlock();
                            logger.info("zkLock02第一次释放锁");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.start();
                Thread thread02 = new Thread(new Runnable() {
    
                    public void run() {
                        try {
                            zkLock03.lock();
                            logger.info("zkLock03第一次获取锁");
                            zkLock03.unlock();
                            logger.info("zkLock03第一次释放锁");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread02.start();
                Thread.sleep(2000);
                zkLock01.unlock();
                logger.info("zkLock01释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public void close(){
            zkClient.close();
        }
    
    }
    

    curator实现及与springboot集成

    	<dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-framework</artifactId>
                <version>2.8.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.curator</groupId>
                <artifactId>curator-recipes</artifactId>
                <version>2.8.0</version>
            </dependency>
    
    @Configuration
    public class CuratorConfiguration {
    
        @Value("${curator.retryCount}")
        private int retryCount;
    
        @Value("${curator.elapsedTimeMs}")
        private int elapsedTimeMs;
    
        @Value("${curator.connectString}")
        private String connectString;
    
        @Value("${curator.sessionTimeoutMs}")
        private int sessionTimeoutMs;
    
        @Value("${curator.connectionTimeoutMs}")
        private int connectionTimeoutMs;
    
        @Bean(initMethod = "start")
        public CuratorFramework curatorFramework() {
            return CuratorFrameworkFactory.newClient(
                    connectString,
                    sessionTimeoutMs,
                    connectionTimeoutMs,
                    new RetryNTimes(retryCount, elapsedTimeMs));
        }
    }
    
    @Service
    public class DistributedLockUtil {
    
        private static final Logger logger = LoggerFactory.getLogger(DistributedLockUtil.class);
    
        private final static String ROOT_PATH_LOCK = "/lock/";
    
        @Autowired
        private CuratorFramework curatorFramework;
    
        /**
         * InterProcessMutex:分布式可重入排它锁
         * InterProcessSemaphoreMutex:分布式排它锁
         * InterProcessReadWriteLock:分布式读写锁
         * InterProcessMultiLock:将多个锁作为单个实体管理的容器
         */
        public InterProcessMutex getLock(String path){
            return new InterProcessMutex(curatorFramework, ROOT_PATH_LOCK + path);
        }
    }
    

    测试代码

    @Service
    public class OrderNumberService {
    
        private int number = 0;
    
        @Autowired
        DistributedLockUtil distributedLockUtil;
    
    
        public String generateNo(){
            String orderNumber ="";
            try {
                Thread.sleep(new Random().nextInt(500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            orderNumber =  "订单号:"+ (++number);
    //        InterProcessMutex lock = distributedLockUtil.getLock("order");
    //        try{
    //            lock.acquire();
    //            orderNumber =  "订单号:"+ (++number);
    //        }catch (Exception e){
    //            e.printStackTrace();
    //        }
    //        finally {
    //            try {
    //                lock.release();
    //            } catch (Exception exception) {
    //                exception.printStackTrace();
    //            }
    //        }
    //
            return orderNumber;
        }
    
    }
    
    
        @Test
        public void testZk(){
            int currency = 200;
            CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);
            for (int i = 0; i <currency ; i++) {
    
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName()+"线程准备好");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    String s = orderNumberService.generateNo();
                    System.out.println(s);
                }).start();
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    比较

    • redis 分布式锁,每次锁释放后,每个client端都会去尝试获取锁。zookeeper可以使用顺序节点解决这个问题。

    • redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

    • redis本身的读写性能很高,因此基于redis的分布式锁效率比较高

    • redis中为了解决单点故障引发的脏数据问题,需要引入redlock,而redlock需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率 ,同时加大了资源的消耗。

    • 个人觉得单从性能考虑或者公司技术栈考虑(毕竟引入redis的概率比zk高很多),可以先择使用redis,其它情况应该选择更靠高的zookeeper,性能要求不高的话可以考虑基于数据库实现分布式锁。

  • 相关阅读:
    JS实现AES加密并与PHP互通的方法分析
    Microsoft: Get started with Dynamic Data Masking in SQL Server 2016 and Azure SQL
    使用php完成常见的文件上传功能
    从php到浏览器的缓存机制
    Hibernate的映射机制是怎样?
    自定义可视化调试工具(Microsoft.VisualStudio.DebuggerVisualizers)
    自适应大小事件
    flex Chrome flash调试时 出现Shockwave flash has crashed的解决办法
    [ActionScript 3] 本地安全沙箱问题最快解决方法
    在C#中调用VBScript和JavaScript等脚本的实现
  • 原文地址:https://www.cnblogs.com/hhhshct/p/14537007.html
Copyright © 2020-2023  润新知