Redis应用之并发处理

一、应用场景

1、多人并发场景,例如秒杀,抢购,设置总数量一百,并发一次减1,减到0为止。

2、单人并发场景,例如连续点击事件,抢票脚本,事件往表插入一条记录,一个用户只能插入一条记录。

二、表结构

-- Create table

create table IPLN_REDIS_TEST

(

id VARCHAR2(32),

key VARCHAR2(100),

value NUMBER,

remark VARCHAR2(100)

)

tablespace TS_VER_EPM

pctfree 10

initrans 1

maxtrans 255

storage

(

initial 64K

next 1M

minextents 1

maxextents unlimited

);

-- Add comments to the table

comment on table IPLN_REDIS_TEST

is 'redis测试';

三、代码实现-使用关系型数据库处理并发

1、多人并发代码,请求成功,数量减1,减到0为止

@ResponseBody

@RequestMapping(value = {"multipleConcurrency.do"}, method = {RequestMethod.POST})

public String multipleConcurrency(@RequestBody Map<String, Object> params){

//校验数据库中是否有key

// ----------采用关系型数据库 处理并发----------

Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

if(keyMap != null && Integer.parseInt(keyMap.get("VALUE").toString()) > 0){

iRedisTestService.updateRedisTest(params);

}else{

return "没有Key,或者并发数为0";

}

return "并发成功,value-1";

}

2、单人并发代码,请求成功,插入一条数据,同一操作人只能插入一条数据

@ResponseBody

@RequestMapping(value = {"singleConcurrency.do"}, method = {RequestMethod.POST})

public String singleConcurrency(@RequestBody Map<String, Object> params){

// ----------采用关系型数据库 处理并发----------

Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

if(keyMap == null){

params.put("value", 1);

params.put("remark", "一个用户并发");

iRedisTestService.insertRedisTest(params);

}else{

return "已有并发数";

}

return "并发成功,插入一条数据";

}

四、PostMan进行测试

1、多用户并发postman测试

2、单用户并发测试

3、测试结果

五、Jmeter并发测试

1、编写测试计划,

单个并发测试,线程数5000并发

多个并发测试,线程数5000并发

2、测试结果

多用户并发测试,扣减为负数,并发处理失败

单用户并发测试,插入了多条记录,并发处理失败

六、Redis处理并发

1、加入redis处理并发,100数量写入redis中

2、代码优化,采用Redis自减指令扣除减1

@ResponseBody

@RequestMapping(value = {"multipleConcurrency.do"}, method = {RequestMethod.POST})

public String multipleConcurrency(@RequestBody Map<String, Object> params){

//校验数据库中是否有key

// ----------采用关系型数据库 处理并发----------

// Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

// if(keyMap != null && Integer.parseInt(keyMap.get("VALUE").toString()) > 0){

// iRedisTestService.updateRedisTest(params);

// }else{

// return "没有Key,或者并发数为0";

// }

// return "并发成功,value-1";

// // ----------采用 Redis 处理并发----------

String key = params.get("key").toString();

Map<Object, Object> keyMap = redisTemplate.opsForHash().entries(key);

if(keyMap != null){

Integer value = Integer.parseInt(keyMap.get("VALUE").toString());

if(value > 0){

long count = redisTemplate.opsForHash().increment(key,"VALUE",-1);

if(count < 0){

return "并发数为0";

}

}else {

return "并发数为0";

}

}else{

return "没有Key";

}

return "并发成功,value-1";

}

3、Jmeter并发测试结果

并发结果并没有变化,还是扣除超限,扣减为负数,并发处理失败

七、原因分析

因为不管是在关系型数据库还是redis中,这里实际上是一个查询库存再扣除库存的操作,减库存时,我们需要判断系统库存够不够,然后才能减掉,这里是两个操作,在大量并发场景下依然会出现问题,因为客户端是多线程的,如果分开独立执行,那么有可能会出现错误,那么我们就需保证两个操作在同一个线程中执行即可,也就是保证它的原子性。如何实现呢?

我们可以采用redis+lua脚本将两步操作放到一起同时在Redis中执行,Redis是单线程操作,故不会出现安全问题。

八、Redis + lua 脚本处理并发

1、lua脚本代码

Lua脚本是redis已经内置的一种轻量小巧语言,用标准C语言编写并以源代码形式开放,其执行是通过redis的eval /evalsha 命令来运行,把操作封装成一个Lua脚本,如论如何都是一次执行的原子操作。

if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then

local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));

if (stock > 0) then

redis.call('hincrby', KEYS[1], KEYS[2], -1);

return stock;

end;

return 0;

end;

2、lua配置类

@Bean

public DefaultRedisScript<Long> stockScript() {

DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

//放在和application.yml 同层目录下

redisScript.setLocation(new ClassPathResource("stock.lua"));

redisScript.setResultType(Long.class);

return redisScript;

}

3、Redis + Lua 代码实现

@ResponseBody

@RequestMapping(value = {"multipleConcurrency.do"}, method = {RequestMethod.POST})

public String multipleConcurrency(@RequestBody Map<String, Object> params){

//校验数据库中是否有key

// ----------采用关系型数据库 处理并发----------

// Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

// if(keyMap != null && Integer.parseInt(keyMap.get("VALUE").toString()) > 0){

// iRedisTestService.updateRedisTest(params);

// }else{

// return "没有Key,或者并发数为0";

// }

// return "并发成功,value-1";

// // ----------采用 Redis 处理并发----------

// String key = params.get("key").toString();

// Map<Object, Object> keyMap = redisTemplate.opsForHash().entries(key);

//

// if(keyMap != null){

// Integer value = Integer.parseInt(keyMap.get("VALUE").toString());

// if(value > 0){

// long count = redisTemplate.opsForHash().increment(key,"VALUE",-1);

// if(count < 0){

// return "并发数为0";

// }

// }else {

// return "并发数为0";

// }

// }else{

// return "没有Key";

// }

// return "并发成功,value-1";

// ----------采用 Redis + Lua 处理并发----------

String key = params.get("key").toString();

List<String> keys = new ArrayList<>();

keys.add(key);

keys.add("VALUE");

Long value = (Long) redisTemplate.execute(defaultRedisScript, keys);

if(value < 0){

return "并发数为0";

}

return "并发成功,value-1";

}

4、测试结果,并没有扣减到负数,并发测试通过

九、单个并发处理,分布式锁

采用Redis分布式锁限制一人一个并发

锁是一种保护机制,在多线程的情况下,保证数据操作的一致性。

分布式锁实现:方式有三种:基于数据库;基于Zookeeper调度中心;基于Redis

分布式锁条件:实现分布式锁要满足3点:多进程可见,互斥,可重入。

1、使用RedisTemplate实现分布式锁

// 加锁

public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {

return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);

}

// 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁

public void unlock(String lockName, String uuid) {

if(uuid.equals(redisTemplate.opsForValue().get(lockName)){ redisTemplate.opsForValue().del(lockName); }

}

// 结构

if(tryLock){

// todo

}finally{

unlock;

}

缺陷:这是锁没错,但get和del操作非原子性,并发一旦大了,无法保证进程安全

2、使用redis+lua实现分布式锁

lua脚本

if redis.call('get', KEYS[1]) == ARGV[1]

then

-- 执行删除操作

return redis.call('del', KEYS[1])

else

-- 不成功,返回0

return 0

end

delete操作时执行Lua命令

// 解锁脚本

DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();

unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));

// 执行lua脚本解锁

redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);

缺陷:不可重入,一个方法获取到锁之后,可能在方法内调这个方法此时就获取不到锁了,必须先释放锁才能再次获取锁。这个时候我们就需要把锁改进成可重入锁了。

3、可重入锁设计

假设锁的key为“ lock ”,hashKey是当前线程的id:“ threadId ”,锁自动释放时间假设为20

获取锁的步骤:

1、判断lock是否存在 EXISTS lock

2、不存在,则自己获取锁,记录重入层数为1.

2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId

3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败.

3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20

释放锁的步骤:

1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId

2、不存在,说明锁已经失效,不用管了

2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,

  3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock

设计lock.lua脚本

local key = KEYS[1]; -- 第1个参数,锁的key

local threadId = ARGV[1]; -- 第2个参数,线程唯一标识

local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间

if(redis.call('exists', key) == 0) then -- 判断锁是否已存在

redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁

redis.call('expire', key, releaseTime); -- 设置有效期

return 1; -- 返回结果

end;

if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己

redis.call('hincrby', key, threadId, '1'); -- 如果是自己,则重入次数+1

redis.call('expire', key, releaseTime); -- 设置有效期

return 1; -- 返回结果

end;

return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

设计unlock.lua脚本

local key = KEYS[1]; -- 第1个参数,锁的key

local threadId = ARGV[1]; -- 第2个参数,线程唯一标识

if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有

return nil; -- 如果已经不是自己,则直接返回

end;

local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1

if (count == 0) then -- 判断是否重入次数是否已经为0

redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除

return nil;

end;

十、Redisson分布式锁

Redisson是一个基于Redis实现的分布式工具,有基本分布式对象和高级又抽象的分布式服务,除了提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。可以让使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

1、引入依赖

<dependency>

<groupId>org.redisson</groupId>

<artifactId>redisson-spring-boot-starter</artifactId>

<version>3.13.6</version>

</dependency>

2、添加配置类

@Configuration

public class RedissionConfig {

@Value("${spring.redis.host}")

private String redisHost;

@Value("${spring.redis.password}")

private String password;

private int port = 6379;

private int database = 1;

@Bean

public RedissonClient getRedisson() {

Config config = new Config();

config.useSingleServer().

setAddress("redis://" + redisHost + ":" + port).

setPassword(password).

setDatabase(database);

config.setCodec(new JsonJacksonCodec());

return Redisson.create(config);

}

}

3、代码改造

@Resource

private RedissonClient redissonClient;

@ResponseBody

@RequestMapping(value = {"singleConcurrency.do"}, method = {RequestMethod.POST})

public String singleConcurrency(@RequestBody Map<String, Object> params){

// ----------采用关系型数据库 处理并发----------

// Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

// if(keyMap == null){

// params.put("value", 1);

// params.put("remark", "一个用户并发");

// iRedisTestService.insertRedisTest(params);

// }else{

// return "已有并发数";

// }

//

// return "并发成功,插入一条数据";

// ----------采用 Redission 处理并发----------

Map<String, Object> keyMap = iRedisTestService.getRedisTestByKey(params);

if(keyMap != null){

return "已有并发数";

}

String lockName = params.get("key").toString();

RLock lock = redissonClient.getLock(lockName);

long expireTime = 20000;

try {

boolean isLocked = lock.tryLock(expireTime, TimeUnit.MILLISECONDS);

if(isLocked){

params.put("value", 1);

params.put("remark", "一个用户并发");

iRedisTestService.insertRedisTest(params);

}

} catch (InterruptedException e) {

lock.unlock();

e.printStackTrace();

}

return "并发成功,插入一条数据";

}

4、压力测试通过

并发量5000,只插入一条数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis是一种高性能的内存数据库,它被广泛用于各种应用场景。以下是几个常见的Redis应用场景: 1. 缓存:由于Redis的高性能和低延迟,它通常被用作数据缓存层,将频繁访问的数据存储在内存中,从而加快数据访问速度。 2. 会话存储:在分布式系统中,可以使用Redis存储用户会话信息,包括用户登录状态、权限等。这样可以轻松实现会话共享和负载均衡。 3. 发布/订阅系统:Redis支持发布/订阅模式,可以用于实时消息传递、事件通知等场景。发布者将消息发布到指定频道,订阅者可以接收到相关消息并做出相应处理。 4. 计数器和排行榜:Redis提供了原子操作的支持,可以用于实现计数器和排行榜功能。例如,可以使用Redis的INCR操作来实现网站的访问计数器,或者根据用户的积分进行排行。 5. 分布式锁:在分布式系统中,为了保证数据的一致性和并发控制,可以使用Redis的分布式锁功能。通过使用Redis的SETNX操作来获取锁,并使用EXPIRE设置锁的过期时间,可以有效地实现分布式锁。 6. 消息队列:Redis的列表结构可以用作轻量级的消息队列。生产者将消息推入列表,消费者则从列表中弹出消息进行处理。这种方式简单高效,适用于需要异步处理的场景。 这只是一部分常见的Redis应用场景,实际上,由于Redis的灵活性和高性能,它在各种场景下都有不同的应用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值