分布式锁主要解决什么问题?:用于解决分布式系统中的并发访问冲突问题。当多个客户端同时对系统中的某个共享资源进行读写操作时,可能会导致数据不一致、资源争用等问题。通过分布式锁技术可以控制同一时刻只有一个客户端对资源进行访问,从而保障数据的一致性和系统的稳定性。
实现分布式锁的方式主要有三种:
1.可以利用数据库唯一索引,因为数据库唯一索引具有天然的排他性。
2.可以利用zookeeper创建瞬时节点,利用zookeeper不可创建重复节点的特性来实现,从而实现桶一时刻只有同一个客户端操作成功。
3.基于redis缓存的方式去实现。这也是我们本篇主讲的一种实现方式,因为前两者都是利用其一个天然特性即可。redis自带原子性,无论多少线程过来访问,redis都只会单个线程去接收。redis的分布式锁主要利用他的put和st去实现。
利用redis去实现分布式锁我们要先知道他需要具备哪些功能:
1.加锁和解锁。
2.失败未超时重试
3.可重入。(如果一个线程对该资源进行加锁后,我们无法再对他加锁的话那就是不可重入的)。
4.避免锁被其他线程提前释放(需要一个锁的检查机制)。
Redis实现分布式锁主要利用到他的setnX来实现分布式锁。特点是当key不存在时候才会去设置值,当key存在则不设置。设置成功返回1,设置设置失败返回0。当然,这种情况只是在redis的客户端操作下,如果用的是jds等这种链接代码和redis的话要另外看具体是哪个中间件返回什么值。
下面我们看redis分布式锁方法具体代码
(注意RedisUtil是自己封装的 包括get和set方法):
/** * 使用Redis的setnx命令实现分布式锁 * @Parom key 加锁的唯一Key * @Parom timeOut 超时时间 单位秒 * @Parom expireTime 锁的有效期 单位秒 该参数作用主要用于避免在服务挂了以后导致的死锁的问题 */ public static boolean tryLock(final String key,final long timeOut,final long expireTime){ long startTime = System.currentTimeMillis(); long endTime = timeOut * 1000; //这里value是自己封装的 RedisUtil.Result value = null; //在没有超时的情况下循环获取锁,实现失败未超时可重试的功能 while (System.currentTimeMillis() - startTime < endTime && value == null){ value = RedisUtil.get(key); } //超时了就是返回失败 if (value == null){ return false; } //如果value不等于null,那么读取成功,并且get到的值为空的话说明锁是空闲状态 if (StringUtils.isEmpty(value.getValue()){ RedisUtil.Result setResult = null; //拿到当前线程的唯一标记值 ; String localValue = getLocalValue(); ; //加锁 同样实现失败未超时可重试的功能 while (System.currentTimeMillis() - startTime < endTime && setResult == null){ RedisUtil.set(key,localValue,expireTime); } //加锁成功,setnx 成功返回1 if (setResult != null && ObjectUtil.equals(setResult.getValue(),"OK")){ //得到当前线程加锁的Value保存起来 currentThreadValue.set(localValue); return true; }else {//超时加锁失败 return false; } } }
下面是生成当前线程唯一标记方法: 当业务代码执行完后我们还需要对锁进行释放:
释放
/** * 用于防止被其他线程非法解锁 */ private static String getLocalValue(){ String hostName = "host_name"; try { hostName = InetAddress.getLocalHost().getHostName(); }catch (Exception e){ throw new RRException("redis乐观锁获取主机名失败"); } return hostName + "_" +Thread.currentThread().getName() + "_" + UUID.randomUUID(); }
锁的前提是当前的线程占用了锁,所以直接读取值,然后拿到threadlocal存的线程唯一标记,进行比较,如果相等,则释放锁。如果不相等,则意味着是其他线程来释放锁(这就是实现线程身份校验功能,防止其他线程释放锁),直接返回失败。
/** * 释放锁 */ public static boolean unLock(final String key){ RedisUtil.Result result = RedisUtil.get(key); String value = currentThreadValue.get(); if (result != null && value.equals(result.getValue())){ long del = RedisUtil.jedis.getResource().del(key); if (del == 1){ currentThreadValue.remove(); }else { return false; } } return false; }