目录
1.业务流程说明
2.结构设计
3.实现思路
4.开整
1.引入依赖
2.配置redisTemplate
3.编写测试类
业务流程要求实现报名是对选项的名额进行限定,如 举办活动的为A, 报名字段中有 性别 (男,女),职业 (老师,学生),且对性别 男限定可报名额为10,女为20, 老师为10 学生为100。该文针对该业务流程进行分析设计,库存方案类似。各位看官一定会举一反三。
使用redis的hash 结构来保存.Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表;
使用以上场景说明,初始化活动A 性别字段 的选项名额. ACTIVITY:1 下的选项 为男(OPTION:M)的库存为10
127.0.0.1:6379> HSET ACTIVITY:A OPTION:SEXY:M 10
(integer) 1
127.0.0.1:6379>
使用HGETALL(获取在哈希表中指定 key 的所有字段和值) 查看是否设置成功;
127.0.0.1:6379> HGETALL ACTIVITY:A
1) "OPTION:SEXY:M"
2) "10"
127.0.0.1:6379>
相同的方式设置性别选项=女 和职业字段的名额.如下
127.0.0.1:6379> HSET ACTIVITY:A OPTION:SEXY:W 20
(integer) 1
127.0.0.1:6379> HSET ACTIVITY:A OPTION:JOB:T 10
(integer) 1
127.0.0.1:6379> HSET ACTIVITY:A OPTION:JOB:S 100
(integer) 1
127.0.0.1:6379> HGETALL ACTIVITY:A
1) "OPTION:SEXY:M"
2) "10"
3) "OPTION:SEXY:W"
4) "20"
5) "OPTION:JOB:T"
6) "10"
7) "OPTION:JOB:S"
8) "100"
127.0.0.1:6379>
使用redis 命令HINCRBY(为哈希表 key 中的指定字段的整数值加上增量 increment ) 来实现名额/库存的扣减操作;具体增减示例
127.0.0.1:6379> HINCRBY ACTIVITY:A OPTION:SEXY:M -5
(integer) 5
127.0.0.1:6379> HGET ACTIVITY:A OPTION:SEXY:M
"5"
127.0.0.1:6379> HINCRBY ACTIVITY:A OPTION:SEXY:M 5
(integer) 10
127.0.0.1:6379> HGET ACTIVITY:A OPTION:SEXY:M
"10"
127.0.0.1:6379>
具体版本依赖项目的版本管理器;
org.springframework.boot
spring-boot-starter-data-redis
设置RedisSerializer 序列化方式是为了方便在redis终端和桌面工具中调试,不设置则使用默认序列化方式,在工具和终端中查询到的是序列化后 不方便阅读;
@ComponentScan("com.evolu.*")
@EnableAutoConfiguration
public class AppConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplateBuilder().build();
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
};
}
import com.evolu.OrderApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
/**
* @Description:
* @Title: RedisTest
* @Package PACKAGE_NAME
* @Author: XX_Dog
* @CreateTime: 2022/10/25 15:50
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrderApplication.class)
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
/**
* 测试并发场景下扣减库存是否存在问题
* @throws InterruptedException
*/
@Test
public void test() throws InterruptedException {
HashOperations ops = redisTemplate.opsForHash();
ops.put("form:1","option:1",0);
Object result = ops.get("form:1","option:1");
System.out.println(result);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
Long incrresult = ops.increment("form:1","option:1",1);
System.out.println("threadA执行结果"+incrresult);
},"threadA"+i);
thread.start();
// thread.join();
}
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
Long incrresult = ops.increment("form:1","option:1",1);
System.out.println("threadB执行结果"+incrresult);
},"threadB"+i);
thread.start();
// thread.join();
}
Object result1 = ops.get("form:1","option:1");
System.out.println(result1);
}
/**
* 扣减库存实际操作
* @throws InterruptedException
*/
@Test
public void testLua() throws InterruptedException {
String resultScript = "if(redis.call('hexists',KEYS[1],KEYS[2]) == 1) \n" +
"\tthen\n" +
"\t\tlocal stock = tonumber(redis.call('hget',KEYS[1],KEYS[2]));\n" +
"\t\tlocal num = tonumber(ARGV[1]);\n" +
"\n" +
"\t\tif(stock >=num) \n" +
"\t\t\tthen\n" +
"\t\t\t\treturn redis.call(\"hincrby\",KEYS[1],KEYS[2],-num);\n" +
"\t\t\tend;\n" +
"\t\treturn 0;\n" +
"\tend;\n" +
" return 1;\n";
List keys = new ArrayList<>();
keys.add("form:1");
keys.add("option:1");
Object result = redisTemplate.execute(new DefaultRedisScript(resultScript,Long.class),keys,"2");
System.out.println(result);
}
}
这里使用lua 脚本的原因是 HINCRBY 虽然是线程安全的,但是无法判断是否超卖,所以我们必须先读到剩余库存在去扣减。在读和扣减这两部操作 并不具备原子性。所以使用Lua脚本来进行读取库存,若充足则进行扣减,不充足则返回;
不喜轻喷 诗书幸有先人业,贫贱初非学者羞