1
在原来的redsi测试服务上,继续来做实验。点击这里
在原有的IRedisService接口上新增两个 分布式的获取锁与释放锁。
/** * 获取锁 * @param lockKey * @param requestId * @param expireTimeSeconds * @return */ boolean getLock(String lockKey, String requestId, long expireTimeSeconds); /** * 释放锁 * @param lockKey * @param requestId * @return */ boolean releaseLock(String lockKey, String requestId);
2 RedisServiceImpl 实现
@Override public boolean getLock(String lockKey, String requestId, long expireTimeSeconds) { return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTimeSeconds, TimeUnit.SECONDS); } @Override public boolean releaseLock(String lockKey, String requestId) { String script = "if redis.call(‘get‘,KEYS[1]) == ARGV[1] then return redis.call(‘del‘,KEYS[1]) else return 0 end"; return (Boolean) this.redisTemplate.execute(new DefaultRedisScript(script, Boolean.class), Collections.singletonList(lockKey), new Object[]{requestId}); }
getLock
在获得锁的时候,opsForValue().setIfAbsent 用到了redis的 setnx特性,这是分布式锁的关键一步,
为了保证锁不被一直占用,要有时间的限制,获取锁同时给了失效时间,保证了原子性。
releaseLock
释放的同时,也要保证原子性,这里用了lua脚本。
3 redis 锁工具类
因为锁是一个经常用到的东西,所以为了方便他人的使用,需要改造成工具类。
我们的redis 是一个service tools是一个class 这时不能直接注入。
需要用springUtil
package com.zhouqiang.demo.tools; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * @author :zhouqiang * @date :2021/8/11 17:32 * @description: * @version: $ */ @Component public class SpringUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; public SpringUtil() { } @Override public void setApplicationContext(ApplicationContext arg0) throws BeansException { applicationContext = arg0; } public static <T> T getBean(String id, Class<T> type) { return applicationContext.getBean(id, type); } public static <T> T getBean(String id, Object params) { return (T) applicationContext.getBean(id, new Object[]{params}); } public static <T> T getBean(Class<T> type) { return applicationContext.getBean(type); } public static <T> T getBeanByParams(Class<T> type, Object... objects) { return applicationContext.getBean(type, objects); } }
这时候,就可以写出完整的 Locl 锁工具类,放在公共的地方,方便其他同事调用。
package com.zhouqiang.demo.tools; import com.zhouqiang.demo.enmu.RedisEnum; import com.zhouqiang.demo.service.redis.IRedisService; import lombok.Getter; import javax.annotation.Resource; import java.util.UUID; /** * @author :zhouqiang * @date :2021/8/11 15:58 * @description:redsi缓存锁 * @version: $ */ public class Lock { private String key; private String requestId; private long second; private Lock(String key, String requestId, long second) { if (second <= 0) { throw new RuntimeException("超时时间必须大于0"); } this.key = key; this.requestId = requestId; this.second = second; } private IRedisService getRedisLockService() { return SpringUtil.getBean(IRedisService.class); } /** * 写一个单例 * * @param redisEnum * @param keys * @return */ public static Lock getInstance(RedisEnum redisEnum, Object... keys) { return new Lock(redisEnum.getKey(), UUID.randomUUID().toString(), redisEnum.getExpiredTime()); } /** * 得到锁 */ public boolean getLock() { return getRedisLockService().getLock(key, requestId, second); } /** * 释放锁 * * @return */ public boolean releaseLock() { return getRedisLockService().releaseLock(key, requestId); } }
既然写了工具类,方便调用。缓存的KEY,也需要规范使用。
还要redis枚举。
package com.zhouqiang.demo.enmu; import lombok.Getter; /** * @author :zhouqiang * @date :2021/8/11 16:27 * @description:缓存枚举 * @version: $ */ @Getter public enum RedisEnum { /** * 商品锁 */ SHOP_CLOCK("SHOP_CLOCK", 10L,"商品锁"); /** * redis的key */ private final String key; /** * 键的过期时间,单位为秒,有效期默认为20秒 */ private final Long expiredTime; /** * key的描述 */ private final String desc; RedisEnum(String key, Long expiredTime, String desc) { this.key = key; this.desc = desc; this.expiredTime = expiredTime; } public static RedisEnum getByKey(String key) { for (RedisEnum value : values()) { if (value.getKey() == key) { return value; } } return null; } }
这样一套下来,基本上redis锁是完成了。
4 业务
随便写一个业务接口。
然后写实现类。
package com.zhouqiang.demo.service.redisDemo.impl; import com.zhouqiang.demo.enmu.RedisEnum; import com.zhouqiang.demo.enmu.ResultCodeEnum; import com.zhouqiang.demo.entity.ResultCode; import com.zhouqiang.demo.service.redis.IRedisService; import com.zhouqiang.demo.service.redisDemo.RedisDemoService; import com.zhouqiang.demo.tools.Lock; import org.redisson.Redisson; import javax.annotation.Resource; import java.util.UUID; /** * @author :zhouqiang * @date :2021/8/12 10:51 * @description: * @version: $ */ public class RedisDemoServiceImpl implements RedisDemoService { @Resource private IRedisService iRedisService; @Resource private Redisson redisson; @Override public String redisDemoTest() { String id = UUID.randomUUID().toString(); //防止操作的事务发生异常造成堵塞 最后都会释放锁 不影响别人操作 Object redis = null; try { //得到锁 5秒失效期 boolean lock = Lock.getInstance(RedisEnum.SHOP_CLOCK, id, 5).getLock(); if (lock == false) { return ResultCodeEnum.LOCK_FAIL.getMsg(); } //要操作的事务(举例) iRedisService.setValue("redis", id); redis = iRedisService.getValue("redis"); } catch (Exception e) { e.printStackTrace(); } finally { //释放锁 万一超时 锁自己没了 不能误删别人的正在进行的锁 Lock.getInstance(RedisEnum.SHOP_CLOCK, id).releaseLock(); } if(redis==null){ return ResultCodeEnum.MESSAGE_FAIL.getMsg(); } return redis.toString(); } }
这里对分布式锁的考虑是比较多的,需要注意很多事项。
1 异常的捕捉
2 缓存的时间
3 锁的释放
这里我还加了返回的枚举,ResultCodeEnum 也做了标准化。
package com.zhouqiang.demo.enmu; import com.zhouqiang.demo.entity.Result; import lombok.Getter; import javax.ws.rs.GET; /** * @author :zhouqiang * @date :2021/8/12 10:34 * @description: * @version: $ */ @Getter public enum ResultCodeEnum { LOCK_SUCCESS(0, "获取锁成功!"), LOCK_FAIL(-1, "获取锁失败!"), MESSAGE_FAIL(-1, "消息失败!"), ; private int code; private String msg; ResultCodeEnum(int code, String msg) { this.code = code; this.msg = msg; } }
5 测试
最后写一个接口测试。这里的返回体用了result封装。
封装了这些,是为了标准化一点。也为了以后好维护。
到此,Redis 分布式锁的就这样了。
BUT redsi 自己封装的一个redisson,更加的好用。
6 redission分布式锁
Redisson 支持单点模式、主从模式、哨兵模式、集群模式 非常的强大啊。
redisson这个框架重度依赖了Lua脚本和Netty,代码很牛逼,各种Future及FutureListener的异步、同步操作转换
实现起来也是非常的容易。
引依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.4.3</version> </dependency>
在业务层只需要几步,
这样就省略了我们1-4 所有的操作。简直就是泪目。
至于redison 有空再去看看源码。到此结束!