1. 分布式中间件技术实战(Java版)
  2. 作者:钟林森 编著
  3. 出版日期 :2019-12-03
  4. ISBN:978-7-111-64151-3
  5. 配书资源下载地址:(请勿直接点击下载,请复制完整路径到迅雷或者浏览器中下载)
  6. http://www.hzcourse.com/oep/image/ueditor/jsp/upload/file/20191209/64151-分布式中间件技术实战(Java版)_源代码+工具.zip

代码在阿里云盘

image.png

3 缓存中间件Redis

由于Redis是基于内存的、采用key-value结构化存储的NoSQL数据库,加上其底层采用单线程和多路I/O复用模型,所以Redis的查询速度还是很快的。

  1. 热点数据的存储与展示
  2. 最近访问的数据:采用Redis的List作为“最近访问的足迹”的数据结构,将大大降低数据库频繁的查询请求。
  3. 并发请求:对于高并发访问的某些数据的情况,Redis可以将这些数据预先装载在缓存中,每次高并发过来的请求则直接从缓存中获取,减少高并发访问给数据库带来的压力。
  4. 排名:采用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);
            }
        }
    }
}