Redis分布式锁案例
概述
分布式锁处理可以采用基于数据库级别的:“乐观锁” 和“悲观锁”实现之外,还可以采用业界比较流行的的方式比如:
- Redisson (企业级)
- curator 分布式锁(企业级)
参考官方文档 官方文档
Redis的典型应用场景
- 热点数据的存储和展示,即大部分频繁访问的数据
- 最近访问的数据存储和展示,即用户访问的最新足迹
- 消息已读,未读,收藏,浏览数,足迹
- 好友关注,粉丝,互关,共同好友
- 限流,黑白名单,抢红包
- 并发访问控制,分布式锁机制
- 排行榜:用来代替传统基于数据库的order by
- 队列机制,基于主题式的发布和订阅,原生的
- 地理定位GEO
- 布隆过滤等
Redis实现分布式锁 — 幂等性问题
在Redis中,可以实现分布式锁的原子操作主要是Set和Expire操作。从redis的2.6x开始,提供了set命令如下:
set key value [EX seconds] [PX milliseconds] [NX|XX]
在改命令中,对应key和value,给为应该很熟悉了。
- EX 是指key的存活时间秒单位
- PX 是指key的存活时间毫秒单位
- NX是指当Key不存在的时候才会设置key值
- XX是指当Key存在时才会设置key的值
从该操作命令不难看出,java setIfAbsent === redis-cli setnx
获取锁1 ,获取不到就是0
- 使用setnx命令获取:“锁”时,如果操作结果返回0.(表示key已经对应的锁)已经不存在,即当前已被其他的线程获取了锁,则获取:“锁”失败,反之获取成功
- 为了防止并发线程在获取锁之后,程序出现异常的情况,从而导致其他线程在调用setnx命令时总是返回1而进入死锁状态,需要为key设置一个“合理”的过期时间
- 当成功获取:“锁”并执行完成相应的操作之后,需要释放该“锁”,可以通过执行del命令讲“锁”删除,而在删除的时候还需要保证所删除的锁,是当前线程所获取的,从而避免出现误删除的情况。
图解
能够实现分布式锁,源自于Redis提供了所有操作的命令均是原子性的,所谓的:“原子性”指的是一个操作要么全部完成,要么全部不完成,每个操作和命令如同一个整体,不可能进行分割。
实现用户注册,使用redis分布式锁解决重复注册问题
依赖
<!-- redis --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions></dependency><dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.8.0</version></dependency>
配置
#redisspring.redis.host=120.77.34.190spring.redis.port=6379spring.redis.password=xxxxxspring.redis.database=15spring.redis.timeout=5000spring.redis.jedis.pool.max-active=20spring.redis.jedis.pool.max-wait=-1spring.redis.jedis.pool.max-idle=30spring.redis.jedis.pool.min-idle=3
sql脚本
-- ------------------------------ Table structure for user_reg-- ----------------------------DROP TABLE IF EXISTS `user_reg`;CREATE TABLE `user_reg` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8 COMMENT='用户注册信息表';
-- ------------------------------ Records of user_reg-- ----------------------------INSERT INTO `user_reg` VALUES ('53', 'linsen', '123456', '2019-04-20 23:01:08');INSERT INTO `user_reg` VALUES ('54', 'debug', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('55', 'debug', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('56', 'jack', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('57', 'sam', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('58', 'jack', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('59', 'jack', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('60', 'sam', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('61', 'sam', '123456', '2019-04-20 23:36:42');INSERT INTO `user_reg` VALUES ('62', 'database', '123456', '2019-04-20 23:59:41');INSERT INTO `user_reg` VALUES ('63', 'rabbitmq', '123456', '2019-04-20 23:59:41');INSERT INTO `user_reg` VALUES ('64', 'lock', '123456', '2019-04-20 23:59:41');INSERT INTO `user_reg` VALUES ('65', 'java', '123456', '2019-04-20 23:59:41');INSERT INTO `user_reg` VALUES ('66', 'redis', '123456', '2019-04-20 23:59:41');INSERT INTO `user_reg` VALUES ('71', 'luohou', '123456', '2019-04-21 21:51:05');INSERT INTO `user_reg` VALUES ('72', 'lixiaolong', '123456', '2019-04-21 21:51:05');INSERT INTO `user_reg` VALUES ('73', 'zhongwenjie', '123456', '2019-04-21 21:51:05');INSERT INTO `user_reg` VALUES ('74', 'userC', '123456', '2019-05-03 15:37:37');INSERT INTO `user_reg` VALUES ('75', 'userD', '123456', '2019-05-03 15:37:37');INSERT INTO `user_reg` VALUES ('76', 'userA', '123456', '2019-05-03 15:37:37');INSERT INTO `user_reg` VALUES ('77', 'userB', '123456', '2019-05-03 15:37:37');
配置类
/** * 通用化配置 * @Author:debug (SteadyJack) * @Date: 2019/3/13 8:38 * @Link:微信-debug0868 QQ-1948831260 **/public class RedisConfiguration {
//Redis链接工厂 @Autowired private RedisConnectionFactory redisConnectionFactory;
/** * 缓存redis-redisTemplate * @return */ @Bean public RedisTemplate<String,Object> redisTemplate(){ RedisTemplate<String,Object> redisTemplate=new RedisTemplate<String, Object>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //TODO:指定大key序列化策略为为String序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer()); //TODO:指定hashKey序列化策略为String序列化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //redisTemplate.setHashValueSerializer(new StringRedisSerializer()); return redisTemplate; }
/** * 缓存redis-stringRedisTemplate * @return */ @Bean public StringRedisTemplate stringRedisTemplate(){ //采用默认配置即可-后续有自定义配置时则在此处添加即可 StringRedisTemplate stringRedisTemplate=new StringRedisTemplate(); stringRedisTemplate.setConnectionFactory(redisConnectionFactory); return stringRedisTemplate; }
}
实现分布式锁
public interface IUserRegService extends IService<UserReg> { // 无锁 void regUserNoLock(UserRegVo userRegVo);
// redis的分布式锁 void regUserRedisLock(UserRegVo userRegVo);}
@Service@Slf4jpublic class UserRegServiceImpl extends ServiceImpl<UserRegMapper, UserReg> implements IUserRegService {
@Autowired private StringRedisTemplate stringRedisTemplate;
@Override public void regUserNoLock(UserRegVo userRegVo) { // 根据用户名查询用户实体信息,如果不存在进行注册 LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>(); // 根据用户名查询对应的用户信息条件 userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName()); // 执行查询语句 UserReg userReg = this.getOne(userRegLambdaQueryWrapper);
if (null == userReg) { // 这里切记一定要创建一个用户 userReg = new UserReg(); // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略 BeanUtils.copyProperties(userRegVo, userReg); // 设置注册时间 userReg.setCreateTime(new Date()); // 执行保存和注册用户方法 this.saveOrUpdate(userReg); } else { // 如果存在就返回用户信息已经存在 throw new RuntimeException("用户信息已经注册存在了..."); } }
@Override public void regUserRedisLock(UserRegVo userRegVo) { // D yykk_lock 1 ?--晚于 A B C // 1: 设置redis的sexnx的key,考虑到幂等性,这个key有如下做法 // 前提是:前userRegVo.getUserName()必须唯一的, // 为什么这样要唯一:A 注册 B也能够注册 String key = userRegVo.getUserName() + "_lock"; // 给每个线程线程产生不同的value,目的是:用于删除锁的判断 String value = System.nanoTime() + "" + UUID.randomUUID(); // 通过java代码设置setnx ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); // 获取锁 //这里又一个隐患,设置太长吞吐量下降,太短可能会造成资源不一致。 这个时间一定要根据实际情况而定 Boolean resLock = opsForValue.setIfAbsent(key, value,20L,TimeUnit.SECONDS); if (resLock) { try { // 根据用户名查询用户实体信息,如果不存在进行注册 LambdaQueryWrapper<UserReg> userRegLambdaQueryWrapper = new LambdaQueryWrapper<>(); // 根据用户名查询对应的用户信息条件 userRegLambdaQueryWrapper.eq(UserReg::getUserName, userRegVo.getUserName()); // 执行查询语句 UserReg userReg = this.getOne(userRegLambdaQueryWrapper); if (null == userReg) { // 这里切记一定要创建一个用户 userReg = new UserReg(); // 使用BeanUtils.copyProperties进行两个对象相同属性的的复制和赋值,如果不同自动忽略 BeanUtils.copyProperties(userRegVo, userReg); // 设置注册时间 userReg.setCreateTime(new Date()); // 执行保存和注册用户方法 this.saveOrUpdate(userReg); } else { // 如果存在就返回用户信息已经存在 throw new RuntimeException("用户信息已经注册存在了..."); } } catch (Exception ex) { throw ex; } finally { // ---------------------释放锁--不论成功还是失败都应该把锁释放掉 // 不管发生任何情况,都一定是自己删除自己的锁 if (opsForValue.get(key).toString().equals(value)) { stringRedisTemplate.delete(key); } } } }}
controller
@RestController@Slf4j@Api(tags = "Redis分布式锁--用户注册")public class UserRegController {
@Autowired private IUserRegService userRegService;
@PostMapping("/user/reg/uuid") public String reg() { return UUID.randomUUID().toString(); }
@ApiOperation("用户注册") @GetMapping("/user/reg") public R reguser(UserRegVo userRegVo) { if (StringUtils.isEmpty(userRegVo.getUserName())) { return new R(701, "请输入用户..."); } if (StringUtils.isEmpty(userRegVo.getPassword())) { return new R(701, "请输入密码..."); }
// 进行用户注册 R response = new R(StatusCode.Success); try { // 无锁版本 //userRegService.regUserNoLock(userRegVo);
// redis分布式锁 userRegService.regUserRedisLock(userRegVo);
} catch (Exception ex) { response = new R(701, "注册用户失败"); }
return response; }
}
小结
- Redis的分布式锁主要采用setnx+ expire命令来完成
- Redis的分布式锁得以实现,主要是得益于Redis的单线程操作机制,即在底层基础架构中,同一时刻,同一个部署节点只允许一个线程执行某种操作,这种操作也称之为原子性。
- 上面的用户注册重复提交的问题,使用了分布式锁的一次性锁,即同一时刻并发的线程锁携带的相同数据只能允许一个线程通过,其他的线程将获取锁失败,而从结束自身的业务流程。