• Redis 分布式锁实现


    在分布式集群环境下,对 Redis 数据的修改也会发生冲突,这时候需要利用锁的机制,防止数据在同一时间被多个系统修改。

    实现分布式锁的思路就是利用 Redis 的两个命令:setnx 和 setex,修改数据前使用 setnx 命令对操作加锁,防止其他系统执行相同操作,使用 setex 命令设置锁超时时间(这一步的目的是防止系统突然挂掉,没有解锁),在操作结束后,进行解锁。

    我们新建一个任务 Scheduled,每10秒执行一次,在不同的机器(虚拟机)上启动相同的项目,因为锁的原因,同一时间只有一个任务被执行,代码如下:

    @Service
    public class LockNxExJob {
    
        private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);
    
        @Autowired
        private RedisService redisService;
        @Autowired
        private RedisTemplate redisTemplate;
    
        private static String LOCK_PREFIX = "prefix_";
    
        @Scheduled(cron = "0/10 * * * * *")
        public void lockJob() {
            String lock = LOCK_PREFIX + "LockNxExJob";
            try{
                //redistemplate setnx操作
                boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
    //试想一下,加入程序运行到这里,系统发生宕机 Object lockValue
    = redisService.get(lock); //获取锁失败 if(!nxRet){ //宕机后,每次获取锁都会失败,这个锁除非人为解锁,否则一直被锁
    String value
    = (String)redisService.get(lock); //打印当前占用锁的服务器IP logger.info("get lock fail,lock belong to:{}",value); return; }else{ redisTemplate.opsForValue().set(lock,getHostIp(),3600); //获取锁成功 logger.info("start lock lockNxExJob success"); Thread.sleep(5000); } }catch (Exception e){ logger.error("lock error",e); }finally { //任务完成后,释放锁 redisService.remove(lock); } } /** * 获取本机内网IP地址方法 * @return */ private static String getHostIp(){ try{ Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces(); while (allNetInterfaces.hasMoreElements()){ NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement(); Enumeration<InetAddress> addresses = netInterface.getInetAddresses(); while (addresses.hasMoreElements()){ InetAddress ip = (InetAddress) addresses.nextElement(); if (ip != null && ip instanceof Inet4Address && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255 && ip.getHostAddress().indexOf(":")==-1){ return ip.getHostAddress(); } } } }catch(Exception e){ e.printStackTrace(); } return null; } }

    上面代码使用到的 RedisService 类

    @Service
    public class RedisService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        private static double size = Math.pow(2, 32);
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @param offset   位 8Bit=1Byte
         * @return
         */
        public boolean setBit(String key, long offset, boolean isShow) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.setBit(key, offset, isShow);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 写入缓存
         *
         * @param key
         * @param offset
         * @return
         */
        public boolean getBit(String key, long offset) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                result = operations.getBit(key, offset);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
    
        /**
         * 写入缓存
         *
         * @param key
         * @param value
         * @return
         */
        public boolean set(final String key, Object value) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 写入缓存设置时效时间
         *
         * @param key
         * @param value
         * @return
         */
        public boolean set(final String key, Object value, Long expireTime) {
            boolean result = false;
            try {
                ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
                operations.set(key, value);
                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 批量删除对应的value
         *
         * @param keys
         */
        public void remove(final String... keys) {
            for (String key : keys) {
                remove(key);
            }
        }
    
    
        /**
         * 删除对应的value
         *
         * @param key
         */
        public void remove(final String key) {
            if (exists(key)) {
                redisTemplate.delete(key);
            }
        }
    
        /**
         * 判断缓存中是否有对应的value
         *
         * @param key
         * @return
         */
        public boolean exists(final String key) {
            return redisTemplate.hasKey(key);
        }
    
        /**
         * 读取缓存
         *
         * @param key
         * @return
         */
        public Object genValue(final String key) {
            Object result = null;
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            result = operations.get(key);
            return result;
        }
    
        /**
         * 哈希 添加
         *
         * @param key
         * @param hashKey
         * @param value
         */
        public void hmSet(String key, Object hashKey, Object value) {
            HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
            hash.put(key, hashKey, value);
        }
    
        /**
         * 哈希获取数据
         *
         * @param key
         * @param hashKey
         * @return
         */
        public Object hmGet(String key, Object hashKey) {
            HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
            return hash.get(key, hashKey);
        }
    
        /**
         * 列表添加
         *
         * @param k
         * @param v
         */
        public void lPush(String k, Object v) {
            ListOperations<String, Object> list = redisTemplate.opsForList();
            list.rightPush(k, v);
        }
    
        /**
         * 列表获取
         *
         * @param k
         * @param l
         * @param l1
         * @return
         */
        public List<Object> lRange(String k, long l, long l1) {
            ListOperations<String, Object> list = redisTemplate.opsForList();
            return list.range(k, l, l1);
        }
    
        /**
         * 集合添加
         *
         * @param key
         * @param value
         */
        public void add(String key, Object value) {
            SetOperations<String, Object> set = redisTemplate.opsForSet();
            set.add(key, value);
        }
    
        /**
         * 集合获取
         *
         * @param key
         * @return
         */
        public Set<Object> setMembers(String key) {
            SetOperations<String, Object> set = redisTemplate.opsForSet();
            return set.members(key);
        }
    
        /**
         * 有序集合添加
         *
         * @param key
         * @param value
         * @param scoure
         */
        public void zAdd(String key, Object value, double scoure) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            zset.add(key, value, scoure);
        }
    
        /**
         * 有序集合获取
         *
         * @param key
         * @param scoure
         * @param scoure1
         * @return
         */
        public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            redisTemplate.opsForValue();
            return zset.rangeByScore(key, scoure, scoure1);
        }
    
    
        //第一次加载的时候将数据加载到redis中
        public void saveDataToRedis(String name) {
            double index = Math.abs(name.hashCode() % size);
            long indexLong = new Double(index).longValue();
            boolean availableUsers = setBit("availableUsers", indexLong, true);
        }
    
        //第一次加载的时候将数据加载到redis中
        public boolean getDataToRedis(String name) {
    
            double index = Math.abs(name.hashCode() % size);
            long indexLong = new Double(index).longValue();
            return getBit("availableUsers", indexLong);
        }
    
        /**
         * 有序集合获取排名
         *
         * @param key 集合名称
         * @param value 值
         */
        public Long zRank(String key, Object value) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            return zset.rank(key,value);
        }
    
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end);
            return ret;
        }
    
        /**
         * 有序集合添加
         *
         * @param key
         * @param value
         */
        public Double zSetScore(String key, Object value) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            return zset.score(key,value);
        }
    
    
        /**
         * 有序集合添加分数
         *
         * @param key
         * @param value
         * @param scoure
         */
        public void incrementScore(String key, Object value, double scoure) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            zset.incrementScore(key, value, scoure);
        }
    
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end);
            return ret;
        }
    
        /**
         * 有序集合获取排名
         *
         * @param key
         */
        public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) {
            ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
            Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end);
            return ret;
        }
    
    }
    RedisService 类似工具类

    当然,这样的做法虽然可以实现锁的功能,但是,无法解决突然的宕机,导致无法解除锁的问题,就像程序中红字标注的情况

    既然这样,是否可以在加锁的同时设置锁的超时时间呢?只要加锁成功,那么锁必然会有超时时间,就可以解决问题了。这里提供两种解决方法。

    第一种方法:使用 lua 脚本

    @Service
    public class LuaDistributeLock {
    
    
        private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);
    
        @Autowired
        private RedisService redisService;
        @Autowired
        private RedisTemplate redisTemplate;
    
        private static String LOCK_PREFIX = "lua_";
    
        private DefaultRedisScript<Boolean> lockScript;
    
    
        @Scheduled(cron = "0/10 * * * * *")
        public void lockJob() {
    
            String lock = LOCK_PREFIX + "LockNxExJob";
    
            boolean luaRet = false;
            try {
                luaRet = luaExpress(lock,getHostIp());
    
                //获取锁失败
                if (!luaRet) {
                    String value = (String) redisService.genValue(lock);
                    //打印当前占用锁的服务器IP
                    //logger.info("lua get lock fail,lock belong to:{}", value);
                    return;
                } else {
                    //获取锁成功
                    //logger.info("lua start  lock lockNxExJob success");
                    Thread.sleep(5000);
                }
            } catch (Exception e) {
                logger.error("lock error", e);
    
            } finally {
                if (luaRet) {
                    //logger.info("release lock success");
                    redisService.remove(lock);
                }
            }
        }
    
    
        /**
         * 获取lua结果
         * @param key
         * @param value
         * @return
         */
        public Boolean luaExpress(String key,String value) {
            lockScript = new DefaultRedisScript<Boolean>();
            lockScript.setScriptSource(
                    new ResourceScriptSource(new ClassPathResource("add.lua")));
            lockScript.setResultType(Boolean.class);
            // 封装参数
            List<Object> keyList = new ArrayList<Object>();
            keyList.add(key);
            keyList.add(value);
            Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
            return result;
        }
    
    
        /**
         * 获取本机内网IP地址方法
         *
         * @return
         */
        private static String getHostIp() {
            try {
                Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
                while (allNetInterfaces.hasMoreElements()) {
                    NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                    Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        InetAddress ip = (InetAddress) addresses.nextElement();
                        if (ip != null
                                && ip instanceof Inet4Address
                                && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                                && ip.getHostAddress().indexOf(":") == -1) {
                            return ip.getHostAddress();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }

    add.lua 脚本的内容如下

    local lockKey = KEYS[1]
    local lockValue = KEYS[2]
    
    -- setnx info
    local result_1 = redis.call('SETNX', lockKey, lockValue)
    if result_1 == true
    then
    local result_2= redis.call('SETEX', lockKey,3600, lockValue)
    return result_1
    else
    return result_1
    end

    脚本文件放置在 resources 下

     

    第二种方法:Redis 原生 API 实现了 setnx 和 setex 两个命令连用的方法,我们就使用这个方法

    引入依赖

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

    代码如下:

    @Component
    public class JedisDistributedLock {
    
        private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
    
        private static String LOCK_PREFIX = "lua_";
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Autowired
        private RedisService redisService;
    
        public static final String UNLOCK_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();
        }
    
    
        @Scheduled(cron = "0/10 * * * * *")
        public void lockJob() {
    
            String lock = LOCK_PREFIX + "JedisNxExJob";
            boolean lockRet = false;
    
            try {
                lockRet = this.setLock(lock, 600);
    
                //获取锁失败
                if (!lockRet) {
                    String value = (String) redisService.genValue(lock);
                    //打印当前占用锁的服务器IP
                    logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
                    return;
                } else {
                    //获取锁成功
                    logger.info("jedisLockJob start  lock lockNxExJob success");
                    Thread.sleep(5000);
                }
            } catch (Exception e) {
                logger.error("jedisLockJob lock error", e);
    
            } finally {
                if (lockRet) {
                    logger.info("jedisLockJob release lock success");
                    redisService.remove(lock);
                }
            }
        }
    
    
        public boolean setLock(String key, long expire) {
            try {
                Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
                    @Override
                    public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                        return connection.set(key.getBytes(), "锁定的资源".getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
                    }
                });
                return result;
            } catch (Exception e) {
                logger.error("set redis occured an exception", e);
            }
            return false;
        }
    
        public String get(String key) {
            try {
                RedisCallback<String> callback = (connection) -> {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    return commands.get(key);
                };
                String result = redisTemplate.execute(callback);
                return result;
            } catch (Exception e) {
                logger.error("get redis occured an exception", e);
            }
            return "";
        }
    
        public boolean releaseLock(String key, String requestId) {
            // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
            try {
                List<String> keys = new ArrayList<>();
                keys.add(key);
                List<String> args = new ArrayList<>();
                args.add(requestId);
    
                // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
                // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
                RedisCallback<Long> callback = (connection) -> {
                    Object nativeConnection = connection.getNativeConnection();
                    // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                    // 集群模式
                    if (nativeConnection instanceof JedisCluster) {
                        return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }
    
                    // 单机模式
                    else if (nativeConnection instanceof Jedis) {
                        return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }
                    return 0L;
                };
                Long result = redisTemplate.execute(callback);
    
                return result != null && result > 0;
            } catch (Exception e) {
                logger.error("release lock occured an exception", e);
            } finally {
                // 清除掉ThreadLocal中的数据,避免内存溢出
                //lockFlag.remove();
            }
            return false;
        }
    
        /**
         * 获取本机内网IP地址方法
         *
         * @return
         */
        private static String getHostIp() {
            try {
                Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
                while (allNetInterfaces.hasMoreElements()) {
                    NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                    Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        InetAddress ip = (InetAddress) addresses.nextElement();
                        if (ip != null
                                && ip instanceof Inet4Address
                                && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                                && ip.getHostAddress().indexOf(":") == -1) {
                            return ip.getHostAddress();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }

    那么现在还有一个问题:假如一个任务的时间特别长,超过了设定的超时时间,此时锁已经自动解除了,那我们最后解除的锁会不会是别人持有的锁?答案是:会的。

    为了解决这个问题,我们在最后解除锁的时候,还得判断下此时的锁是否还是当初的锁。代码如下(注意加粗的部分):

    @Component
    public class JedisDistributedLock {
    
        private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);
    
        private static String LOCK_PREFIX = "JedisDistributedLock_";
    
        private DefaultRedisScript<Boolean> lockScript;
    
        @Resource
        private RedisTemplate<Object, Object> redisTemplate;
    
        @Autowired
        private RedisService redisService;
    
        public static final String UNLOCK_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();
        }
    
    
        @Scheduled(cron = "0/10 * * * * *")
        public void lockJob() {
    
            String lock = LOCK_PREFIX + "JedisNxExJob";
            boolean lockRet = false;
            try {
                lockRet = this.setLock(lock, 600);
    
                //获取锁失败
                if (!lockRet) {
                    String value = (String) redisService.genValue(lock);
                    //打印当前占用锁的服务器IP
                    logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
                    return;
                } else {
                    //获取锁成功
                    logger.info("jedisLockJob start  lock lockNxExJob success");
                    Thread.sleep(5000);
                }
            } catch (Exception e) {
                logger.error("jedisLockJob lock error", e);
    
            } finally {
                if (lockRet) {
                    logger.info("jedisLockJob release lock success");
                    releaseLock(lock,getHostIp());
                }
            }
        }
    
    
        public boolean setLock(String key, long expire) {
            try {
                Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
                    @Override
                    public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                        return connection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
                    }
                });
                return result;
            } catch (Exception e) {
                logger.error("set redis occured an exception", e);
            }
            return false;
        }
    
        public String get(String key) {
            try {
                RedisCallback<String> callback = (connection) -> {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    return commands.get(key);
                };
                String result = redisTemplate.execute(callback);
                return result;
            } catch (Exception e) {
                logger.error("get redis occured an exception", e);
            }
            return "";
        }
    
        /**
         * 释放锁操作
         * @param key
         * @param value
         * @return
         */
        private boolean releaseLock(String key, String value) {
            lockScript = new DefaultRedisScript<Boolean>();
            lockScript.setScriptSource(
                    new ResourceScriptSource(new ClassPathResource("unlock.lua")));
            lockScript.setResultType(Boolean.class);
            // 封装参数
            List<Object> keyList = new ArrayList<Object>();
            keyList.add(key);
            keyList.add(value);
            Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
            return result;
        }
    
        /**
         * 获取本机内网IP地址方法
         *
         * @return
         */
        private static String getHostIp() {
            try {
                Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
                while (allNetInterfaces.hasMoreElements()) {
                    NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                    Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        InetAddress ip = (InetAddress) addresses.nextElement();
                        if (ip != null
                                && ip instanceof Inet4Address
                                && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                                && ip.getHostAddress().indexOf(":") == -1) {
                            return ip.getHostAddress();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    }

    unlock.lua 脚本的内容如下

    local lockKey = KEYS[1]
    local lockValue = KEYS[2]
    
    -- get key
    local result_1 = redis.call('get', lockKey)
    if result_1 == lockValue
    then
    local result_2= redis.call('del', lockKey)
    return result_2
    else
    return false
    end

    常见面试题:

    问题一:什么是分布式锁?实现分布式锁的注意点?

    同一时间同一资源只能被同一个应用操作。

    首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下三个条件:

    • 互斥性。在任意时刻,只有一个客户端能持有锁。
    • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

    问题二:怎么实现分布式锁?

    1、采用lua脚本操作分布式锁

    • 从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
    • Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。

    2、采用 setnx、setex 命令连用的方式实现分布式锁

    问题三:解锁需要注意什么?

    解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

  • 相关阅读:
    StringBuffer和StringBuilder
    String类
    软件设计师考点总结(一)
    基于 Axis2的webService接口的基本开发步骤
    Linux学习Day10:LVM(逻辑卷管理器)
    Linux学习Day9:使用RAID(独立冗余磁盘阵列)
    Linux学习Day8:挂载硬盘设备、添加交换分区
    Linux学习Day7:用户身份与文件权限、su命令与sudo服务
    Linux学习Day6:编写Shell脚本、配置计划任务
    Linux学习Day5:Vim编辑器、配置网卡、配置Yum软件本地仓库
  • 原文地址:https://www.cnblogs.com/jwen1994/p/12252719.html
Copyright © 2020-2023  润新知