redis循环键_Redis的并发控制
背景
开发活动报名业务,涉及到活动人数限制的问题,当并发量上来的时候,多人同时提交报名信息,将会导致活动已报名人数的不准确,对业务造成影响,如下图:
分析出现问题的原因是,设置操作发生的时候,并没有确保当前人数的准确性,即没有确保当前查询出来的已报名人数与数据库的一致性,导致客户端并发的两次操作有被覆盖的情况发生
传统数据库 VS NoSql
mysql
针对如上场景,若报名人数字段保存在mysql数据库中,可以使用一种常见的降低读写锁冲突,保证数据一致性的乐观锁机制(Compare and Set CAS),实现方案如下
将原来的操作sql代码 update act set num=#{numNew} where actId=#{actId}
复制代码
改为 update act set num=#{numNew} where actId=#{actId} and num=#{numOld}
复制代码
即只有当查询出来的数据与当前数据库的数据一致时,才可以进行赋值操作,否则失败
redis
若使用redis,则活动报名人数以键值对的形式存在内存中,业务代码将会对内存中的人数进行操作,相比mysql,redis的效率更高,不会造成很大的延迟(若当并发量很大时,使用mysql进行报名人数记录,CAS的方案将会导致很多客户端操作失败,用户体验不好),但使用redis,其没有很好的事务支持,以上mysql的解决方案不能很好的运用在redis上,因此如何设计redis锁,进行共享资源(已报名活动人数)的操作,是需要解决的问题
使用到的命令说明
设计Redis锁之前,需要介绍下即将用到的几个命令
SETNX
将key设置值为value,如果key不存在,这种情况下等同SET命令,返回值1。 当key存在时,什么也不做,返回值0。
watch && MULTI
watch:标记所有指定的key 被监视起来,在事务中有条件的执行(乐观锁)
MULTI:标记一个事务块的开始。 随后的指令将在执行EXEC时作为一个原子执行
当两者一起使用的时候,首先key被watch监视,若在调用 EXEC 命令执行事务时, 如果任意一个被监视的键被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。如下表:
时间
客户端A
客户端B
T1
WATCH name
T2
MULTI
T3
SET name owen
T4
SET name tom
T5
EXEC
在时间 T4 ,客户端 B 修改了 name 键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name 这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败。
GETSET
GETSET key value 返回之前的旧值value,之后设置key的新值
Redis基本解决思路以及遇到的问题
以下列举使用redis锁的基本思路
注:例子使用spring-data-redis库,setnx命令变为setIfAbsent,并且返回true or false private StringRedisTemplate stringRedisTemplate;
public Boolean setConcurrentLock(String key) throws InterruptedException {
ValueOperations ops = stringRedisTemplate.opsForValue();
while (!ops.setIfAbsent(key, "lock"))) {
TimeUnit.MILLISECONDS.sleep(3);
}
return true;
}
public void deleteConcurrentLock(String key) {
stringRedisTemplate.delete(key);
}
复制代码
如上获取redis锁使用了setnx命令,若lock被占用,则返回false,一直循环,直到lock被删除后可以赋值成功,才能获得锁,实现对共享资源加锁。
但是,很明显,while存在死循环死锁的可能,当如下场景:
线程1获取到lock,线程2,线程3在执行while循环等待lock删除,若线程1突然挂掉,没能删除lock,则导致线程2,线程3死循环,死锁
想到解决方案为对锁设置超时,防止无限制循环,代码如下: private StringRedisTemplate stringRedisTemplate;
public Boolean setConcurrentLock(String key, long expireTime) throws InterruptedException {
ValueOperations ops = stringRedisTemplate.opsForValue();
//expireTime 为锁超时时间
while (!ops.setIfAbsent(key, String.valueOf(System.currentTimeMillis() + expireTime))) {
Long expire = Long.parseLong(ops.get(key));
//判断是否超时
if (expire != null && expire < System.currentTimeMillis()) {
//getset获取旧的时间,并且设置新的超时时间
Long oldExpire = Long.parseLong(ops.getAndSet(key, String.valueOf(System.currentTimeMillis() + expireTime)));
if (oldExpire != null && oldExpire < System.currentTimeMillis()) {
break;
}
}
TimeUnit.MILLISECONDS.sleep(3);
}
return true;
}
public void deleteConcurrentLock(String key) {
stringRedisTemplate.delete(key);
}
复制代码
若获取锁失败,进入while循环,判断超时时间是否已到,if判断为真,证明lock已经超时。所以执行getset命令,获取旧的时间,并设置新的超时时间,若获取的旧的时间超时了,则证明获取lock成功,跳出循环
但此处添加超时控制仍然存在问题,如下场景
场景一:
线程1获取lock并且挂掉,线程2,线程3 进入while循环后,同时判断出lock已经超时,线程2首先执行getset命令,返回了线程1设置的超时时间,确实超时,线程2获取锁;线程3执行getset命令,返回了线程2设置的超时时间,并未超时,但是线程3重新设置了超时时间
场景二:
有关删除锁的方法,若线程2持有锁期间超时,但是操作没有执行完,锁被线程3重新设置,变为线程3的锁,线程2执行完毕后,直接执行del,则会把线程3的锁删除,出现问题
Redis最终实践方案
针对上面列举的两个问题,修改代码的最终实践版如下: private StringRedisTemplate stringRedisTemplate;
public static ThreadLocal holder = new ThreadLocal<>();
public Boolean setConcurrentLock(String key, long expireTime) throws InterruptedException {
ValueOperations ops = stringRedisTemplate.opsForValue();
while (!ops.setIfAbsent(key, String.valueOf(System.currentTimeMillis() + expireTime))) {
stringRedisTemplate.watch(key);
Long expire = Long.parseLong(ops.get(key));
if (expire != null && expire < System.currentTimeMillis()) {
stringRedisTemplate.multi();
Long oldExpire = Long.parseLong(ops.getAndSet(key, String.valueOf(System.currentTimeMillis() + expireTime)));
if (stringRedisTemplate.exec() != null && oldExpire != null && oldExpire < System.currentTimeMillis()) {
break;
}
} else {
stringRedisTemplate.unwatch();
}
TimeUnit.MILLISECONDS.sleep(3);
}
holder.set(ops.get(key));
return true;
}
public void deleteConcurrentLock(String key) {
ValueOperations ops = stringRedisTemplate.opsForValue();
Long expire = Long.valueOf(ops.get(key));
if(exprie.equals(holder.get())){
stringRedisTemplate.delete(key);
}
holder.remove();
}
复制代码
如上面的场景一,首先线程2,线程3同时判断出lock超时后,对lock进行watch监视,然后将getset操作放到事务中执行,若线程2执行完事务,修改了lock的时间后,线程3由于执行事务命令lock被修改而失败,不会覆盖设置线程2的超时时间,解决场景一问题
对于场景二,为了防止已经超时的线程误删其他正在执行的线程lock,引入ThreadLock变量,将本线程设置的超时时间放入ThreadLock中,若删除的时候,从Redis取出的时间变化了,证明该线程超时,时间被其他线程重新设置过,就不需要删除lock。最后需要注意的是使用ThreadLocal需要在判断是够删除lock锁时手动删除,防止web服务器中的线程池对线程复用,造成ThreadLocal重复使用。
总结
本篇实践是基于单点redis服务器情况下的锁(若工程在多机器下部署,可以装逼的叫redis分布式锁)。但在redis集群架构下,如果master节点down机,由于redis主从复制是异步的,会有明显的race-condition。Redis文档中提供了一种解决方案:RedLock,后续有机会再去实践学习吧。。。
redis循环键_Redis的并发控制相关推荐
- redis循环键_Redis 性能优化的 13 条军规!史上最全
Redis性能优化实战方案 Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,尽管 Redis 使用了非阻塞式 IO,并且对各种命令都做了优化(大部分命令 ...
- redis字符串匹配_Redis的数据类型和抽象概念介绍
Redis 不是一个 简单的 key-value 存储,实际上它是一个数据结构服务器,它支持不同类型的值.也就是说,在传统的key-value存储中,你将一个字符串的key关联到一个字符串的值上:而在 ...
- java redis 主从 哨兵_Redis主从复制与哨兵机制
Redis主从复制 1.redis的复制功能是支持多个数据库之间的数据同步.一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数 ...
- redis 管理工具_Redis的跨平台GUI 桌面管理工具
RedisDesktopManager 是一个快速.简单.支持跨平台的 Redis 桌面管理工具,基于 Qt 5 开发,支持通过 SSH Tunnel 连接. 快速安装 可以直接在github上下载 ...
- redis setnx 原子性_Redis从入门到深入-分布式锁(26)
1. 分布式锁 1.1 简介 锁 是一种用来解决多个执行线程 访问共享资源 错误或数据不一致问题的工具 如果 把一台服务器比作一个房子,那么 线程就好比里面的住户,当他们想要共同访问一个共享资源,例如 ...
- 两台服务器安装redis集群_Redis Cluster搭建高可用Redis服务器集群
一.Redis Cluster集群简介 Redis Cluster是Redis官方提供的分布式解决方案,在3.0版本后推出的,有效地解决了Redis分布式的需求,当一个节点挂了可以快速的切换到另一个节 ...
- java redis 网络断开_Redis长时间连接后自动断开
从日志看2小时 [DEBUG] 22:02:48.206 org.nutz.ioc.impl.NutIoc.get(NutIoc.java:151) - Get 'emailAlertService' ...
- redis value最大值_Redis 的 maxmemory 和 dbnum 默认值都是多少?对于最大值会有限制吗?...
一.Redis 的默认配置 了解 Redis 的都知道,Redis 服务器状态有很多可配置的默认值. 例如:数据库数量,最大可用内存,AOF 持久化相关配置和 RDB 持久化相关配置等等.我相信,关于 ...
- java redis 主从配置_Redis实现主从复制(MasterSlave)
Redis实现主从复制(Master&Slave) Redis主从复制 1.是什么 1.单机有什么问题: 单机故障 容量瓶颈 qps瓶颈 主机数据更新后根据配置和策略,自动同步到备机的mast ...
最新文章
- 艾伟:memcached全面剖析–3.memcached的删除机制和发展方向
- vim上次和下次光标位置
- Discrete Logarithm is a Joke __int128 浮点数e
- 三个实例演示 Java Thread Dump 日志分析
- Rotation Matching CodeForces - 1365C(贪心)
- 计算机文化基础分析总结,《计算机文化基础实训》教学方案设计与课题分析总结.doc...
- enumset_枚举集合的EnumSet
- 计算机应用技术一级考试成绩,《计算机应用基础》课程与等级考试成绩的关系...
- vue 中的动态传参和query传参
- 前端学习(804):替换字符串和转换为数组
- python爬虫源码项目_32个Python爬虫实战项目,满足你的项目慌(带源码)
- 实验计算机控制器的实验结论,微机控制实验报告
- gis怎么提取水系_ArcGIS水文分析实战教程(7)细说流域提取
- 解决安装TortoiseSVN时,提示 Windows-Update(kb2999226)
- Linux学习笔记——SecureCRT 8版本中文破解版
- 淘宝客淘宝联盟解析二合一链接获取优惠链接还原二合一,提取优惠信息
- Ubuntu用apt-get下载csh
- 使用React.js和appbase.io构建类似Twitter的Search Feed
- 终端模拟器 java_程序员必备之终端模拟器,让你的终端世界多一抹“颜色”
- CISAW信息安全保证人员介绍