1.前言
对每个controller来说都是全新且单独的,原因是多线程,如果多个请求操作共有的数据,这样的并发操作会导致脏数据
怎么解决?
mysql可以使用积极锁解决,
这里讲解的是redis的解决办法,虽然有几种解决办法,但我这里只记录最好的:setnx指令算法加锁,思路与mysql的消极锁相似
2.redis锁需要满足几个要求:
(1)只能让一个客户端加锁,当锁存在时其他客户端不可以加锁
(2)只能让加锁的客户端解锁,不允许其他客户端解锁
(3)当锁存在时,加锁失败的客户端需要等待解锁后自己加锁,只有自己加锁成功后才可以操作共有数据,即阻塞操作
(4)不能产生死锁,如果加锁的客户端还没有解锁前就因为某些原因就崩溃了,锁自动解锁,不影响其他客户端操作
3.原理
(1)加锁 则是 使用setnx指令,新建一个键值对,如果成功会返回 OK ,即视为加锁成功,如果已经存在,则返回空 ,视为加锁失败
(2)为了确保不产生死锁,应该对这个数据设置存活时间,如果崩溃后,到时间会自动删除
(3)解锁 则是 使用Lua脚本语句对键值对判断这个锁是不是自己加的,如果时别人的或者不存在,则不操作,如果是自己的则做删除键值对操作
(4)无论时加锁还是解锁操作,为了确保逻辑的原子性,都应该是个多参数操作指令,有些低版本的redis不支持,才会将指令拆分成多条顺序执行
但是容易导致逻辑问题,形成脏数据,
4.封装工具
我做的一个工具类,输入参数即可完成加锁 解锁操作 ,这是单机的,如果是分布式则改将jedis对象改成分布式对象ShardedJedisPool即可,思路一样
package cn.cen2guo.clinic.redis; import redis.clients.jedis.Jedis; import java.util.Collections; public class JedisLock { //加锁检查 //加锁标识,返回结果是这个则说明加锁成功 private static final String LOCK_SUCCESS = "OK"; //定义set方法的使用方式,如果不存在则以 lockKey 为 key ,requestId 为value 新建string类型键值 , private static final String SET_IF_NOT_EXIST = "NX"; //表示开启存活时间,PX是毫秒数 ,如果想以秒为单位则设为EX private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取锁[加锁] * * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { //语句意思是,判断lockKey 这个key是否存在,不存在则以 lockKey 为 key ,requestId 为value 新建string类型键值 , //并开启存活时间,单位毫秒, //新建成功返回结果 OK ,说明加锁成功 // 失败则为空,说明加锁失败 String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } // // // // // // // // // //解锁标识,返回结果是这个则说明解锁成功 private static final Long RELEASE_SUCCESS = 1L; /** * 释放锁 * * @param jedis Redis客户端 * * @param lockKey 锁 * * @param requestId 请求标识 * * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { // 使用Lua脚本,执行判断语句, //删除成功则返回结果1L,失败则为0 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(lockKey), Collections.singletonList(requestId)); System.out.println("解锁结果是=="+result); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
5.使用
加锁
/** * 给等待池加锁 */ //锁的key名,自定义 String lockKey = "lock" ; //唯一识别是哪个用户端的锁id // 标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)。 //时间戳+随机数 String requestId = System.currentTimeMillis() + UUID.randomUUID().toString(); //存活时间,10秒,防止崩溃造成死锁 int expireTime = 10000; //获取jedis对象 Jedis jedis = jedisMyGetJedis.myGetJedis(); //加锁操作 boolean f = JedisLock.tryGetDistributedLock(jedis, lockKey, requestId, expireTime); if (!f) { // 失败,锁已经存在 //休眠0.5秒后再次加锁操作 System.out.println("加锁失败,锁已经存在" + new Date()); int k = 0; while (k == 0) { System.out.println("睡眠一次" + new Date()); //当前线程休眠0.5秒 Thread.sleep(500); boolean f2 = JedisLock.tryGetDistributedLock(jedis, lockKey, requestId, expireTime); if (f2) { //加锁成功 // 退出循环 k = 1; } } } //加锁成功 System.out.println("加锁成功" + new Date()); //下面的操作只会有一个人操作,因此不需要担心有并发操作
解锁
//解锁 boolean r = JedisLock.releaseDistributedLock(jedis, lockKey, requestId); if (r) { System.out.println("解锁成功"); } else { System.out.println("解锁失败"); } //关闭jedis对象 jedis.close();