背景

开发活动报名业务,涉及到活动人数限制的问题,当并发量上来的时候,多人同时提交报名信息,将会导致活动已报名人数的不准确,对业务造成影响,如下图:

分析出现问题的原因是,设置操作发生的时候,并没有确保当前人数的准确性,即没有确保当前查询出来的已报名人数与数据库的一致性,导致客户端并发的两次操作有被覆盖的情况发生

传统数据库 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的并发控制相关推荐

  1. redis循环键_Redis 性能优化的 13 条军规!史上最全

    Redis性能优化实战方案 Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,尽管 Redis 使用了非阻塞式 IO,并且对各种命令都做了优化(大部分命令 ...

  2. redis字符串匹配_Redis的数据类型和抽象概念介绍

    Redis 不是一个 简单的 key-value 存储,实际上它是一个数据结构服务器,它支持不同类型的值.也就是说,在传统的key-value存储中,你将一个字符串的key关联到一个字符串的值上:而在 ...

  3. java redis 主从 哨兵_Redis主从复制与哨兵机制

    Redis主从复制 1.redis的复制功能是支持多个数据库之间的数据同步.一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数 ...

  4. redis 管理工具_Redis的跨平台GUI 桌面管理工具

    RedisDesktopManager 是一个快速.简单.支持跨平台的 Redis 桌面管理工具,基于 Qt 5 开发,支持通过 SSH Tunnel 连接. 快速安装 可以直接在github上下载 ...

  5. redis setnx 原子性_Redis从入门到深入-分布式锁(26)

    1. 分布式锁 1.1 简介 锁 是一种用来解决多个执行线程 访问共享资源 错误或数据不一致问题的工具 如果 把一台服务器比作一个房子,那么 线程就好比里面的住户,当他们想要共同访问一个共享资源,例如 ...

  6. 两台服务器安装redis集群_Redis Cluster搭建高可用Redis服务器集群

    一.Redis Cluster集群简介 Redis Cluster是Redis官方提供的分布式解决方案,在3.0版本后推出的,有效地解决了Redis分布式的需求,当一个节点挂了可以快速的切换到另一个节 ...

  7. java redis 网络断开_Redis长时间连接后自动断开

    从日志看2小时 [DEBUG] 22:02:48.206 org.nutz.ioc.impl.NutIoc.get(NutIoc.java:151) - Get 'emailAlertService' ...

  8. redis value最大值_Redis 的 maxmemory 和 dbnum 默认值都是多少?对于最大值会有限制吗?...

    一.Redis 的默认配置 了解 Redis 的都知道,Redis 服务器状态有很多可配置的默认值. 例如:数据库数量,最大可用内存,AOF 持久化相关配置和 RDB 持久化相关配置等等.我相信,关于 ...

  9. java redis 主从配置_Redis实现主从复制(MasterSlave)

    Redis实现主从复制(Master&Slave) Redis主从复制 1.是什么 1.单机有什么问题: 单机故障 容量瓶颈 qps瓶颈 主机数据更新后根据配置和策略,自动同步到备机的mast ...

最新文章

  1. 艾伟:memcached全面剖析–3.memcached的删除机制和发展方向
  2. vim上次和下次光标位置
  3. Discrete Logarithm is a Joke __int128 浮点数e
  4. 三个实例演示 Java Thread Dump 日志分析
  5. Rotation Matching CodeForces - 1365C(贪心)
  6. 计算机文化基础分析总结,《计算机文化基础实训》教学方案设计与课题分析总结.doc...
  7. enumset_枚举集合的EnumSet
  8. 计算机应用技术一级考试成绩,《计算机应用基础》课程与等级考试成绩的关系...
  9. vue 中的动态传参和query传参
  10. 前端学习(804):替换字符串和转换为数组
  11. python爬虫源码项目_32个Python爬虫实战项目,满足你的项目慌(带源码)
  12. 实验计算机控制器的实验结论,微机控制实验报告
  13. gis怎么提取水系_ArcGIS水文分析实战教程(7)细说流域提取
  14. 解决安装TortoiseSVN时,提示 Windows-Update(kb2999226)
  15. Linux学习笔记——SecureCRT 8版本中文破解版
  16. 淘宝客淘宝联盟解析二合一链接获取优惠链接还原二合一,提取优惠信息
  17. Ubuntu用apt-get下载csh
  18. 使用React.js和appbase.io构建类似Twitter的Search Feed
  19. 终端模拟器 java_程序员必备之终端模拟器,让你的终端世界多一抹“颜色”
  20. CISAW信息安全保证人员介绍

热门文章

  1. 关闭和启动Oracle的几种方法总结
  2. 程序员的进阶课-架构师之路(9)-平衡二叉树(AVL树)
  3. 思维导图系列之MySQL知识梳理
  4. Spring Boot Logback 配置详解
  5. Linux下安装Elasticsearch6.x
  6. Android四级缓存,RecyclerView的四级缓存-初探
  7. 延迟任务调度系统—技术选型与设计(上篇)
  8. NIOS2随笔——BMP解码与VGA显示
  9. Linux安装Java
  10. “10%时间”:优点和缺点——敏捷海滩会议上Elizabeth Pope的报告