分布式中间件技术实战(Java版)作者:钟林森 编著出版日期 :2019-12-03ISBN:978-7-111-64151-3配书资源下载地址:(请勿直接点击下载,请复制完整路径到迅雷或者浏览器中下载)http://www.hzcourse.com/oep/image/ueditor/jsp/upload/file/20191209/64151-分布式中间件技术实战(Java版)_源代码+工具.zip
代码在阿里云盘
3 缓存中间件Redis
由于Redis是基于内存的、采用key-value结构化存储的NoSQL数据库,加上其底层采用单线程和多路I/O复用模型,所以Redis的查询速度还是很快的。
- 热点数据的存储与展示
- 最近访问的数据:采用Redis的List作为“最近访问的足迹”的数据结构,将大大降低数据库频繁的查询请求。
- 并发请求:对于高并发访问的某些数据的情况,Redis可以将这些数据预先装载在缓存中,每次高并发过来的请求则直接从缓存中获取,减少高并发访问给数据库带来的压力。
- 排名:采用Redis的有序集合可以很好地实现用户的排名,避免了传统的基于数据库级别的Order By和Group By查询带来的性能问题。
3.2Redis的使用
windows下载是这个地址 | https://github.com/tporadowski/redis/releases
windows下载地址|https://github.com/MicrosoftArchive/redis/releases
redis命令文档|https://redis.io/commands | http://www.redis.cn/commands.html
3.2.3 SpringBoot项目整合Redis
定义JSON序列化与反序列化框架,这个有意思,之前不曾用过。
//定义JSON序列化与反序列化框架
@Autowired
private ObjectMapper objectMapper;
@Test
void contextLoads() throws JsonProcessingException {
final User user = new User(1, "debug", "测试");
final ValueOperations opsForValue = redisTemplate.opsForValue();
final String value = objectMapper.writeValueAsString(user);
opsForValue.set("redis:template:one", value);
final Object get = opsForValue.get("redis:template:one");
if (get != null) {
final User redisUser = objectMapper.readValue(get.toString(), User.class);
log.info("{}", redisUser);
}
}
3.3.2 列表
Redis的列表类型跟Java类型很类似,用于存储具有相似类型的数据,其底层对于数据的存储和读取可以理解为一个“数据队列”,往List中添加数据时,即相当于往队列中的某个位置插入数据;而从list中获取数据时,即相当于从队列中某个位置获取数据。
下面演示一个实际场景:将一组已经排序好的用户对象列表存储在缓存中,按照排名的先后顺序获取出来并打印到控制台上。
//列表类型
@Test
void testList() {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person(1, 21, "修罗", "debug", "火星"));
list.add(new Person(2, 22, "大圣", "jack", "水帘洞"));
list.add(new Person(3, 23, "盘古", "lee", "上古"));
log.info("构造好的list:{}", list);
final String key = "redis:test:2";
//将列表存储到list
final ListOperations listOperations = redisTemplate.opsForList();
for (Person person : list) {
//往列表中添加数据-从队尾中添加
listOperations.leftPush(key, person);
}
//获取redis中的list数据-从对头中遍历获取,直到没有元素为止。
log.info("获取redis中的list数据");
Object res = listOperations.rightPop(key);
Person resp;
while (res != null) {
resp = (Person) res;
log.info("当前数据:{}", resp);
res = listOperations.rightPop(key);
}
}
使用List类型时,可以通过push添加、pop获取等操作存储获取的数据。在实际应用场景中,Redis的列表特别适合排名,排行榜,近期访问数据列表等业务场景。
3.3.3 集合
Redis中的集合Set存储的数据是唯一的,其底层的数据结构是通过哈希表来实现的,所以其添加、删除、查找的复杂度均为O(1)。
以下演示需求为:给定数组要求剔除具有相同姓名的人员并组成新的集合,存放至缓存中。
//集合类型
@Test
void set(){
//构造一组用户姓名列表
final ArrayList<String> userList = new ArrayList<>();
userList.add("debug");
userList.add("jack");
userList.add("修罗");
userList.add("大圣");
userList.add("debug");
userList.add("jack");
userList.add("修罗");
userList.add("大圣");
log.info("待处理用户姓名列表:{}",userList);
final String key="redis:test:3";
final SetOperations setOperations = redisTemplate.opsForSet();
for (String s : userList) {
setOperations.add(key,s);
}
//从缓存中获取用户对象集合
Object pop = setOperations.pop(key);
while (pop!=null){
log.info("从缓存中获取当前用户集合-当前用户:{}",pop);
pop=setOperations.pop(key);
}
}
Redis的集合类型确实可以保证存储的数据是惟一的、不重复的。在实际的场景中,Redis的set类型常用于解决重复提交、剔除重复ID等业务场景。
3.3.4 有序集合
Redis的有序集合SortedSet跟集合Set具有某些相同的特性,即存储的数据是不重复、无序、唯一的;而这两者的不同之处在于SortedSet可以通过底层的Score(分数/权重)值对数据进行排序,实现存储的集合数据既不重复又有序。可以说是包含了List、集合Set的特性。
演示:找出一个一星期内手机充值话费单次充值金额前6名的用户列表。
@Test
public void sortedSet() {
//构造一个无序的用户充值对象
List<PhoneUser> list = new ArrayList<>();
list.add(new PhoneUser("103", 130.0));
list.add(new PhoneUser("101", 120.0));
list.add(new PhoneUser("102", 80.0));
list.add(new PhoneUser("105", 70.0));
list.add(new PhoneUser("106", 50.0));
list.add(new PhoneUser("104", 150.0));
log.info("构造的无序手机充值对象:{}", list);
final String key = "redis:test:4";
final ZSetOperations zSet = redisTemplate.opsForZSet();
for (PhoneUser phoneUser : list) {
//将元素添加到有序集合中
zSet.add(key, phoneUser, phoneUser.getFare());
}
//获取访问充值排名的用户列表
final Long size = zSet.size(key);
//从小到大排序
final Set<PhoneUser> range = zSet.range(key, 0L, size);
//从大到小排序
final Set<PhoneUser> reverseRange = zSet.reverseRange(key, 0L, size);
for (PhoneUser phoneUser : range) {
log.info("从缓存中读取手机充值排序列表,当前记录{}", phoneUser);
}
}
@Setter
@Getter
@ToString
public class PhoneUser implements Serializable {
private String phone;
private Double fare;
public PhoneUser() {
}
public PhoneUser(String phone, Double fare) {
this.phone = phone;
this.fare = fare;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PhoneUser phoneUser = (PhoneUser) o;
return new org.apache.commons.lang.builder.EqualsBuilder().append( phone, phoneUser.phone ).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37 ).append( phone ).toHashCode();
}
}
默认情况下。Redis的有序集合类型SortedSet确实可以实现数据元素的排列。默认排序类型是根据得分Score参数从小到大排序。如果需要倒叙排列,可以调用reverseRange方法即可。
Redis的有序集合SortedSet常用于充值排行榜、积分排行版、成绩排名等应用场景。
3.3.5 哈希Hash存储
Redis的哈希存储跟Java的hashMap数据结构有点类似。其地城结构是有key-value组成的映射。
@Test
void hash(){
//构造学生对象和水果列表
final ArrayList<Student> students = new ArrayList<>();
final ArrayList<Fruit> fruits= new ArrayList<>();
students.add(new Student("10010","debug","大圣"));
students.add(new Student("10011","jack","修罗"));
students.add(new Student("10012","sam","上古"));
fruits.add(new Fruit("apple","红色"));
fruits.add(new Fruit("orange","橙色"));
fruits.add(new Fruit("banana","黄色"));
final String sKey="redis:test:5";
final String fKey="redis:test:6";
//获取hash 存储操作组件 hashOperation ,遍历获取集合中的对象并添加进缓存中
final HashOperations hash = redisTemplate.opsForHash();
for (Student s : students) {
hash.put(sKey,s.getId(),s);
}
for (Fruit f : fruits) {
hash.put(fKey,f.getName(),f);
}
//获取学生和水果
final Map<String, Student> sMap = hash.entries(sKey);
log.info("获取学生对象列表:{}",sMap);
final Map<String, Fruit> fMap = hash.entries(fKey);
//获取指定的学生对象
final Student stu = (Student) hash.get(sKey, "10012");
}
4 Redis典型应用场景抢红包
感觉这个分布式锁比较巧妙1 是使用redis 存在就不设置保证了用户和红包1对1 2 是 使用Redis集合pop保证了不用全局加锁,保证了效率。
public BigDecimal rob(Integer userId, String redId) throws Exception {
ValueOperations valueOperations = redisTemplate.opsForValue();
//用户是否抢过该红包
Object obj = valueOperations.get(redId + userId + ":rob");
if (obj != null) {
return new BigDecimal(obj.toString());
}
//"点红包"
Boolean res = click(redId);
if (res) {
//上锁:一个红包每个人只能抢一次随机金额;一个人每次只能抢到红包的一次随机金额 即要永远保证 1对1 的关系
final String lockKey = redId + userId + "-lock";
Boolean lock = valueOperations.setIfAbsent(lockKey, redId);
redisTemplate.expire(lockKey, 24L, TimeUnit.HOURS);
try {
if (lock) {
//"抢红包"-且红包有钱
Object value = redisTemplate.opsForList().rightPop(redId);
if (value != null) {
//红包个数减一
String redTotalKey = redId + ":total";
Integer currTotal = valueOperations.get(redTotalKey) != null ? (Integer) valueOperations.get(redTotalKey) : 0;
valueOperations.set(redTotalKey, currTotal - 1);
//将红包金额返回给用户的同时,将抢红包记录入数据库与缓存
BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
redService.recordRobRedPacket(userId, redId, new BigDecimal(value.toString()));
valueOperations.set(redId + userId + ":rob", result, 24L, TimeUnit.HOURS);
log.info("当前用户抢到红包了:userId={} key={} 金额={} ", userId, redId, result);
return result;
}
}
} catch (Exception e) {
throw new Exception("系统异常-抢红包-加分布式锁失败!");
}
}
return null;
}
5 消息中间件RabbitMQ
RabbitMQ在一些典型的应用场景和业务模块处理中具有重要作用,比如业务模块解耦,异步通信,高并发限流,超时业务和数据延时处理。
5.1.2 典型应用场景
RabbitMQ作为一款实现高性能存储分发消息的分布式消息,具有异步通信、服务解耦,接口限流、消息分发和业务延迟处理等功能。
1 异步通信和服务解耦
7 锁
用户注册Redis锁
/**
* 处理用户提交注册的请求-加分布式锁
* @param dto
* @throws Exception
*/
public void userRegWithLock(UserRegDto dto) throws Exception{
//精心设计并构造SETNX操作中的Key-一定要跟实际的业务或共享资源挂钩
final String key=dto.getUserName()+"-lock";
//设计Key对应的Value
//为了具有随机性,在这里采用系统提供的纳秒级别的时间戳 + UUID生成的随机数作为Value
final String value=System.nanoTime()+""+UUID.randomUUID();
//获取操作Key的ValueOperations实例
ValueOperations valueOperations=stringRedisTemplate.opsForValue();
//调用SETNX操作获取锁,如果返回true,则获取锁成功
//代表当前的共享资源还没被其他线程所占用
Boolean res=valueOperations.setIfAbsent(key,value);
//返回true,即代表获取到分布式锁
if (res){
//为了防止出现死锁的状况,加上EXPIRE操作,即Key的过期时间,在这里设置为20s
//具体应根据实际情况而定
stringRedisTemplate.expire(key,20L, TimeUnit.SECONDS);
try {
//根据用户名查询用户实体信息
UserReg reg=userRegMapper.selectByUserName(dto.getUserName());
//如果当前用户名还未被注册,则将当前用户信息注册入数据库中
if (reg==null){
log.info("---加了分布式锁---,当前用户名为:{} ",dto.getUserName());
//创建用户注册实体信息
UserReg entity=new UserReg();
//将提交的用户注册请求实体信息中对应的字段取值
//复制到新创建的用户注册实体的相应字段中
BeanUtils.copyProperties(dto,entity);
//设置注册时间
entity.setCreateTime(new Date());
//插入用户注册信息
userRegMapper.insertSelective(entity);
}else {
//如果用户名已被注册,则抛出异常
throw new Exception("用户信息已经存在!");
}
}catch (Exception e){
throw e;
}finally {
//不管发生任何情况,都需要在redis加锁成功并访问操作完共享资源后释放锁
if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}
}
}
}
